UNPKG

@docfy/ember-vite

Version:

Vite plugin for Docfy Ember integration with @embroider/vite

144 lines (143 loc) 6.47 kB
import plugin from '@docfy/core/lib/plugin.js'; import visit from 'unist-util-visit'; import findNode from 'unist-util-find'; import toString from 'mdast-util-to-string'; import path from 'path'; import { generateDemoComponentName, getExt, createDemoNodes, deleteNode, isDemoComponents, } from './utils.js'; /* * Create the heading for the examples section of the page. * * It uses the remark stack to make sure any plugins that manage headings can be * executed. * * This is necessary for apps using remark-autolink-headings, for example. */ function createHeading(ctx) { const heading = ctx.remark.runSync(ctx.remark.parse('## Examples')).children[0]; heading.depth = 2; return heading; } const isTextMarker = (node) => node.type === 'paragraph' && node.children.length === 1 && node.children[0].type === 'text'; const demoMarkerRegex = /^\[\[demo:(.+?)\]\]$/; const demoMarker = (node) => isTextMarker(node) && demoMarkerRegex.test(node.children[0].value); const demosAllMarkerRegex = /^\[\[demos-all\]\]$/; const demosAllMarker = (node) => isTextMarker(node) && demosAllMarkerRegex.test(node.children[0].value); /* * Insert Demo nodes into the page. */ function insertDemoNodesIntoPage(page, toInsert) { if (Array.isArray(page.ast.children)) { const secondHeading = findNode(page.ast, (node) => node.type === 'heading' && node.depth !== 1); if (secondHeading) { const index = page.ast.children.findIndex(el => el === secondHeading); page.ast.children.splice(index, 0, ...toInsert); } else { page.ast.children.push(...toInsert); } } } function replaceDemoMarkers(page, demos) { if (Array.isArray(page.ast.children)) { const markers = []; const allMarkers = []; visit(page.ast, 'paragraph', (node) => { if (demoMarker(node)) markers.push(node); if (demosAllMarker(node)) allMarkers.push(node); }); markers.forEach(marker => { const child = marker.children[0]; const matches = child.value.match(demoMarkerRegex); if (!matches) return; // TODO: This is an inner loop and can cause perf issues if someone // out there has many demos on a single page. It would be better to // create a demo component hash that can be looked up by demo name. const demoName = matches[1]; const demo = demos.find(d => d.name.dashCase.endsWith(demoName)); if (!demo) { console.warn(`Found demo marker "${demoName}" with no matching demo component in ${page.source}`); return; } marker.type = 'div'; marker.children.splice(0, 1, ...createDemoNodes(demo)); }); allMarkers.forEach(marker => { const demoNodes = demos.map(component => createDemoNodes(component)).flat(); marker.type = 'div'; marker.children.splice(0, 1, ...demoNodes); }); } } export default plugin({ runWithMdast(ctx) { const seenNames = new Set(); ctx.pages.forEach(page => { if (page.demos) { const demoComponents = []; page.demos.forEach(demo => { const chunks = []; visit(demo.ast, 'code', (node) => { if (['component', 'template', 'styles'].includes(node.meta || '')) { chunks.push({ snippet: node, code: node.value.replace(/\\{{/g, '{{'), // un-escape hbs ext: getExt(node.lang || (node.meta === 'template' ? 'hbs' : 'js')), type: node.meta, }); } }); // 1. exclude extension // 2. remove /index.md because of web conventions const baseName = page.source.replace('/index.md', '').split('.')[0]; const componentName = generateDemoComponentName(`docfy-demo-${baseName}-${path.basename(demo.source).split('.')[0]}`, seenNames); const demoTitle = findNode(demo.ast, (node) => node.type === 'heading' && node.depth === 1); if (demoTitle) { demoTitle.depth = 3; demoTitle.data = { ...(demoTitle.data || {}), id: componentName.dashCase, docfyDelete: true, // mark the heading to be deleted by @docfy/core TOC plugin }; } demoComponents.push({ name: componentName, chunks, description: { title: demoTitle ? toString(demoTitle) : undefined, ast: demo.ast, editUrl: demo.meta.editUrl, }, }); // Delete used code blocks chunks.forEach(({ snippet }) => { deleteNode(demo.ast.children, snippet); }); }); if (page.meta.frontmatter.manualDemoInsertion) { // Manual demo insertion inserts demos into markdown files // wherever there is a demo marker ([[demo:name]] or [[demos-all]]) replaceDemoMarkers(page, demoComponents); } else { // Automatic demo insertion creates an Example block after // the first heading. const toInsert = [createHeading(ctx)]; demoComponents.forEach(component => { toInsert.push(...createDemoNodes(component)); }); insertDemoNodesIntoPage(page, toInsert); } const pluginData = page.pluginData; if (isDemoComponents(pluginData.demoComponents)) { pluginData.demoComponents.push(...demoComponents); } else { pluginData.demoComponents = demoComponents; } } }); }, });