UNPKG

@custom-elements-manifest/to-markdown

Version:

Custom-elements.json is a file format that describes custom elements. This format will allow tooling and IDEs to give rich information about the custom elements in a given project. It is, however, very experimental and things are subject to change. Follow

251 lines (212 loc) 10.2 kB
import { html, heading, inlineCode, root, table, tableCell, tableRow, text } from 'mdast-builder'; import { capital, repeat, compose, identity, isPrivate, isProtected, isStatic, isLengthy, kindIs, and, not, or, trace, isDefined } from './lib/fp.js'; import * as CELLS from './lib/cells.js'; import { serialize } from './lib/serialize.js'; const line = html('<hr/>'); const DECLARATIONS = { mixins: 'mixins', variables: 'variables', functions: 'functions', exports: 'exports' }; const SECTIONS = { mainHeading: 'main-heading', superClass: 'super-class', fields: 'fields', methods: 'methods', staticFields: 'static-fields', staticMethods: 'static-methods', slots: 'slots', events: 'events', attributes: 'attributes', cssProperties: 'css-properties', cssParts: 'css-parts', mixins: 'mixins' } /** Options -> Declaration -> Heading */ const declarationHeading = options => ({ kind, name, tagName }) => heading( 2 + (options?.headingOffset ?? 0), [ text(`${kind}: `), inlineCode(name), ...tagName ? [ text(', '), inlineCode(tagName) ] : [] ] ); /** String -> Descriptor */ const defaultDescriptor = name => ({ heading: capital(name), get: x => x?.[name] }); /** String|Descriptor -> Descriptor */ const getDescriptor = x => typeof x === 'string' ? defaultDescriptor(x) : x; /** Options -> [Declaration] -> Descriptor -> Column */ const getColumnWithOptions = options => decls => ({ heading, get, cellType = text }) => ({ heading, cellType, values: decls.map(x => get(x, options)) }) /** Column -> Cell */ const getHeading = x => tableCell(text(x.heading)); /** Int -> Column -> Cell */ const getCell = i => ({ values, cellType = text }) => { const value = values[i]; if (!value) return tableCell(text('')); if (cellType === 'raw') return tableCell(value); else return tableCell(cellType(value ?? '')); } /** [Column] -> (, Int) -> Row [Cell] */ const getRows = columns => (_, i) => tableRow(columns.map(getCell(i))); /** * Options -> String -> [String|Descriptor] -> [Declaration] -> Parent Table * @type {import("./types/main").CurriedTableFn} */ const tableWithTitle = options => { const getColumn = getColumnWithOptions(options); return (title, names, _decls, { headingLevel = 3, filter } = { }) => { const by = ( typeof filter === 'function' ? filter : options?.private === 'hidden' ? not(isPrivate) : options?.private === 'details' ? not(or(isPrivate, isProtected)) : identity ); const decls = (_decls ?? []).filter(by).filter(identity); if (!isLengthy(decls)) return []; // xs.map(compose(g, f)) === xs.map(f).map(g) const columns = names.map(compose(getColumn(decls), getDescriptor)) const contentRows = decls.map(getRows(columns)); return [ heading(headingLevel + (options?.headingOffset ?? 0), text(title)), table( repeat(columns.length, null), [ tableRow(columns.map(getHeading)), ...contentRows ] ), ]; } } function optionEnabled(option) { return isDefined(option) && option === true; } function getOmittedConfig(type, omittedOptions) { // target will either be the declarations options object or the sections options object, depending on `type` const target = type === 'decl' ? Object.assign({}, DECLARATIONS) : Object.assign({}, SECTIONS); // rewrite target object with boolean values for comparison in nodes array // true if not omitted, false if omitted. Object.keys(target).forEach((omitted) => target[omitted] = !omittedOptions.includes(target[omitted])); return target; } /** Declaration[] -> Declaration[] */ function filteredDeclarations(declarationsToFilter, ommitedDeclarations, classNameFilter) { // run classNameFilter function if present const actualClassNameFilter = classNameFilter instanceof Function ? new RegExp(classNameFilter()) : new RegExp(classNameFilter); return declarationsToFilter.filter((decl) => { if(kindIs('class')(decl)) { return actualClassNameFilter.test(decl.name); } else if(kindIs('mixin')(decl)) { return isDefined(ommitedDeclarations.mixins) && ommitedDeclarations.mixins === true; } else if(kindIs('variable')(decl)) { return isDefined(ommitedDeclarations.variables) && ommitedDeclarations.variables === true; } else if(kindIs('function')(decl)) { return isDefined(ommitedDeclarations.functions) && ommitedDeclarations.functions === true; } return keepDeclaration.every((result) => result === true); }); } /** @type {import("./types/main").MakeModuleDocFn} */ function makeModuleDoc(mod, options) { const declarations = mod?.declarations ?? []; const exportsDecl = mod?.exports ?? []; if (!declarations.length && !exportsDecl.length) return; const { headingOffset = 0, classNameFilter = '.*', omitSections = [], omitDeclarations = [], } = options ?? {}; const omittedSections = getOmittedConfig('section', omitSections); const omittedDeclarations = getOmittedConfig('decl', omitDeclarations); const makeTable = tableWithTitle(options); const makeHeading = declarationHeading(options); const variablesDecl = filteredDeclarations(declarations, omittedDeclarations, classNameFilter).filter(kindIs('variable')); const functionsDecl = filteredDeclarations(declarations, omittedDeclarations, classNameFilter).filter(kindIs('function')); return [ optionEnabled(omittedSections.mainHeading) ? heading(1 + headingOffset, [inlineCode(mod.path), text(':')]) : null, ...(filteredDeclarations(declarations, omittedDeclarations, classNameFilter).flatMap(decl => { const { kind, members = [] } = decl; const fieldsDecl = members.filter(and(kindIs('field'), not(isStatic))); const methodsDecl = members.filter(and(kindIs('method'), not(isStatic))); const staticFieldsDecl = members.filter(and(kindIs('field'), isStatic)); const staticMethodsDecl = members.filter(and(kindIs('method'), isStatic)); const nodes = [ !['mixin', 'class'].includes(kind) ? null : makeHeading(decl), ...optionEnabled(omittedSections.superClass) ? makeTable('Superclass', [CELLS.NAME, 'module', 'package'], [decl.superclass]) : [], ...optionEnabled(omittedSections.mixins) ? makeTable('Mixins', [CELLS.NAME, 'module', 'package'], decl.mixins) : [], ...kind === 'mixin' && optionEnabled(omittedSections.mixins) ? makeTable('Parameters', [CELLS.NAME, CELLS.TYPE, CELLS.DEFAULT, 'description'], decl.parameters) : [], ...optionEnabled(omittedSections.staticFields) ? makeTable('Static Fields', [CELLS.NAME, 'privacy', CELLS.TYPE, CELLS.DEFAULT, 'description', CELLS.INHERITANCE], staticFieldsDecl) : [], ...optionEnabled(omittedSections.staticMethods) ? makeTable('Static Methods', [CELLS.NAME, 'privacy', 'description', CELLS.PARAMETERS, CELLS.RETURN, CELLS.INHERITANCE], staticMethodsDecl) : [], ...optionEnabled(omittedSections.fields) ? makeTable('Fields', [CELLS.NAME, 'privacy', CELLS.TYPE, CELLS.DEFAULT, 'description', CELLS.INHERITANCE], fieldsDecl) : [], ...optionEnabled(omittedSections.methods) ? makeTable('Methods', [CELLS.NAME, 'privacy', 'description', CELLS.PARAMETERS, CELLS.RETURN, CELLS.INHERITANCE], methodsDecl) : [], ...optionEnabled(omittedSections.events) ? makeTable('Events', [CELLS.NAME, CELLS.TYPE, 'description', CELLS.INHERITANCE], decl.events) : [], ...optionEnabled(omittedSections.attributes) ? makeTable('Attributes', [CELLS.NAME, CELLS.ATTR_FIELD, CELLS.INHERITANCE], decl.attributes) : [], ...optionEnabled(omittedSections.cssProperties) ? makeTable('CSS Properties', [CELLS.NAME, CELLS.DEFAULT, 'description'], decl.cssProperties) : [], ...optionEnabled(omittedSections.cssParts) ? makeTable('CSS Parts', [CELLS.NAME, 'description'], decl.cssParts) : [], ...optionEnabled(omittedSections.slots) ? makeTable('Slots', [CELLS.NAME, 'description'], decl.slots) : [], ].filter(identity); if ( options?.private === 'details' && ( isLengthy(fieldsDecl.filter(or(isPrivate, isProtected))) || isLengthy(methodsDecl.filter(or(isPrivate, isProtected))) ) ) { nodes.push( html('<details><summary>Private API</summary>'), ...makeTable('Fields', [CELLS.NAME, 'privacy', CELLS.TYPE, CELLS.DEFAULT, 'description', CELLS.INHERITANCE], fieldsDecl.filter(or(isPrivate, isProtected)), { filter: identity }), ...makeTable('Methods', [CELLS.NAME, 'privacy', 'description', CELLS.PARAMETERS, CELLS.RETURN, CELLS.INHERITANCE], methodsDecl.filter(or(isPrivate, isProtected)), { filter: identity }), html('</details>') ); } if (nodes.length) nodes.push(line); return nodes; })), ...variablesDecl.length && optionEnabled(omittedDeclarations.variables) ? makeTable('Variables', [CELLS.NAME, 'description', CELLS.TYPE], variablesDecl, { headingLevel: 2} ) : [], ...variablesDecl.length && optionEnabled(omittedDeclarations.variables) ? [line] : [], ...functionsDecl.length && optionEnabled(omittedDeclarations.functions) ? makeTable('Functions', [CELLS.NAME, 'description', CELLS.PARAMETERS, CELLS.RETURN], functionsDecl, { headingLevel: 2} ) : [], ...functionsDecl.length && optionEnabled(omittedDeclarations.functions) ? [line] : [], ...optionEnabled(omittedDeclarations.exports) ? makeTable('Exports', [CELLS.EXPORT_KIND, CELLS.NAME, CELLS.DECLARATION, CELLS.MODULE, CELLS.PACKAGE], mod.exports, { headingLevel: 2} ) : [], ].filter(identity) } /** * Renders a custom elements manifest as Markdown * @param {import('custom-elements-manifest/schema').Package} manifest * @param {import('./types/main').Options} manifest * @return {string} */ export function customElementsManifestToMarkdown(manifest, options) { const tree = root(manifest.modules .flatMap(x => makeModuleDoc(x, options)) .filter(identity)) return serialize(tree); }