From c5a5e6606949d36815a69f8f648cfff25308aa8a Mon Sep 17 00:00:00 2001 From: mbehzad Date: Mon, 15 Apr 2024 17:19:21 +0200 Subject: [PATCH] feat(pv-scripts): add option to use the styleguide's example markup without modification introduce the `raw` option that can be set via params of the code block, front matter of the markdown or config.stlyemark.yaml's `examples` section and won't wrap the markup with `htmlHead`, `bodyHtml` etc from the config when set. Also introduce a new experimental pattern for how the code blocks can be written to be picked up by the stylemark. i.e. `css example-1 hidden` or `html example-1 ./demo/file.html?foo=bar#anchor raw=true hidden` fix #227 --- .../tasks/lsg/buildLsgExamples.js | 27 ++++-- packages/pv-stylemark/tasks/lsg/getLsgData.js | 86 ++++++++++++++++--- 2 files changed, 92 insertions(+), 21 deletions(-) diff --git a/packages/pv-stylemark/tasks/lsg/buildLsgExamples.js b/packages/pv-stylemark/tasks/lsg/buildLsgExamples.js index 23306b8..49c53f9 100644 --- a/packages/pv-stylemark/tasks/lsg/buildLsgExamples.js +++ b/packages/pv-stylemark/tasks/lsg/buildLsgExamples.js @@ -23,19 +23,32 @@ const buildComponentExample = async (config, lsgData, exampleData, template) => try { let componentMarkup = ""; if (exampleData.exampleMarkup.examplePath) { - const componentPath = resolveApp(join(destPath, "components", lsgData.componentPath, exampleData.exampleMarkup.examplePath + ".html")); + const componentPath = resolveApp(join(destPath, "components", lsgData.componentPath, exampleData.exampleMarkup.examplePath)); componentMarkup = await readFile(componentPath, { encoding: "utf-8" }); } else { componentMarkup = exampleData.exampleMarkup.content; } const configBodyHtml = config.examples?.bodyHtml ?? "{html}"; componentMarkup = configBodyHtml.replace(/{html}/g, componentMarkup); - const markup = template({ - lsgData, - componentMarkup, - exampleStyles: exampleData.exampleStyles, - lsgConfig: config, - }); + // when the `raw` parameter is set in stylemark config, or the markdowns frontmatter or via the parameters of the code block in the markdown, + // the markup will be used as it is and not wrapped by stylemark generated markup + const useMarkupRaw = Object.assign({}, config.examples, lsgData.options, exampleData.exampleMarkup.params).raw; + let markup = ""; + if (useMarkupRaw) { + const styles = exampleData.exampleStyles.map(style => ``).join("\n"); + const scripts = exampleData.exampleScripts.map(script => ``).join("\n"); + markup = componentMarkup + .replace("", `${styles}\n`) + .replace("", `${scripts}\n`); + } else { + markup = template({ + lsgData, + componentMarkup, + exampleStyles: exampleData.exampleStyles, + exampleScripts: exampleData.exampleScripts, + lsgConfig: config, + }); + } await writeFile(destPath, "styleguide", `${lsgData.componentName}-${exampleData.exampleName}`, markup); } catch (error) { console.warn(error); diff --git a/packages/pv-stylemark/tasks/lsg/getLsgData.js b/packages/pv-stylemark/tasks/lsg/getLsgData.js index 98b4d36..a7abb37 100644 --- a/packages/pv-stylemark/tasks/lsg/getLsgData.js +++ b/packages/pv-stylemark/tasks/lsg/getLsgData.js @@ -11,11 +11,19 @@ const { resolveApp, getAppConfig } = require("../../helper/paths"); * @typedef {Object} StyleMarkCodeBlock * @property {string} exampleName - will be used to identify the html page rendered as an iframe * @property {string} [examplePath] - optional, will be a relative path to the html file (relative from target/components/path/to/markdown) + * @property {string} [search] - optional, the query params coming after the path in the code block. example: `?foo=bar` (? is part of the value) + * @property {string} [hash] - optional, hash value coming after the path in the code block e.g. `#anchor` (# is part of the value) * @property {"html"|"css"|"js"} language - `html` will create a new html page, `js` and `css` will be added in the html file - * @property {"" | " hidden"} hidden - Indicates whether the code block should also be shown in the styleguide description of the component * @property {string} content - the content of the code block + * @property {Object} params + * @property {boolean} [params.hidden] - Indicates whether the code block should also be shown in the styleguide description of the component + * @property {boolean} [params.raw] - Indicates whether the html needs to be wrapped by stylemark or rendered as it comes, raw. * @example - * ```exampleName:examplePath.lang hidden + * ```exampleName:examplePath.language hidden + * content + * ``` + * // new pattern + * ```language exampleName examplePath[search][hash] hidden raw=false * content * ``` */ @@ -39,7 +47,7 @@ const { resolveApp, getAppConfig } = require("../../helper/paths"); * }} StyleMarkLSGData */ -// example code blocks +// example code blocks: // ```example:/path/to/page.html // ``` // @@ -52,7 +60,22 @@ const { resolveApp, getAppConfig } = require("../../helper/paths"); // display: none; // } // ``` -const regexExecutableCodeBlocks = /``` *(?[\w\-]+)(:(?(\.?\.\/)*[\w\-/]+))?\.(?html|css|js)(?( hidden)?) *\n+(?[^```]*)```/g; +const legacyRegexExecutableCodeBlocks = /``` *(?[\w\-]+)(:(?(\.?\.\/)*[\w\-/]+))?\.(?html|css|js)(?( .*)?) *\n+(?[^```]*)```/g; + +// example code blocks: +// ```html example ./path/to/page.html +// ``` +// +// ```js example +// console.log('Example 1: ' + data); +// ``` +// +// ```css example hidden +// button { +// display: none; +// } +// ``` +const regexExecutableCodeBlocks = /``` *(?html|css|js) (?[\w\-]+)( +(?(\.?\.\/)*[\w\-/]+\.[\w\-/]+))?(?\?.+?)?(?#.+?)?(?( .*))? *\n+(?[^```]*)```/g const exampleParser = { name: "exampleParser", @@ -161,11 +184,42 @@ const getLsgData = async (curGlob, config) => { * extracts the fenced code blocks from the markdown that are meant to be used in the example pages according to the stylemark spec (@link https://github.com/mpetrovich/stylemark/blob/main/README-SPEC.md) * * @param {string} markdownContent - * @returns {Array} + * @returns {StyleMarkCodeBlock[]} + */ +function getExecutableCodeBlocks(markdownContent) { + return [ + ...markdownContent.matchAll(legacyRegexExecutableCodeBlocks), + ...markdownContent.matchAll(regexExecutableCodeBlocks), + ].map(match => normalizeRegexGroups(match.groups)); +} + +/** + * the `groups` object of the regex for the executable code blocks, will be modified to have the object exactly how it is needed and not what is possible using only regex. + * this includes nested objects and boolean casting + * @param {object} groups + * @param {string} [groups.examplePath] + * @param {string} [groups.params] + * @param {string} groups.exampleName + * @param {string} groups.language + * @param {string} [groups.content] + * @returns {StyleMarkCodeBlock} */ -async function getExecutableCodeBlocks(markdownContent) { - return Array.from(markdownContent.matchAll(regexExecutableCodeBlocks)) - .map(match => match.groups); +function normalizeRegexGroups(groups) { + // "type=module hidden" --> `{ type: "module", hidden: true }` + groups.params = Object.fromEntries((groups.params ?? "").trim().split(" ").map(part => part.trim()).filter(part => part !== "").map(part => { + let [key, value] = part.split("="); + // for boolean, cast + if (value === "true") value = true; + if (value === "false") value = false; + return [key, value ?? true]; + })); + + if (groups.examplePath) { + // in the new pattern, the extension is part of examplePath. in the old one the extension is used for the `language` instead. + groups.examplePath = groups.examplePath.match(/\.[\w\-]+$/) ? groups.examplePath : `${groups.examplePath}.${groups.language}`; + } + + return groups; } /** @@ -176,18 +230,18 @@ async function getExecutableCodeBlocks(markdownContent) { * @returns {string} */ function cleanMarkdownFromExecutableCodeBlocks(markdownContent, name, componentPath) { - return markdownContent.replace(regexExecutableCodeBlocks, (...args) => { + function replacer(...args) { let replacement = ""; /** @type {StyleMarkCodeBlock} */ - const groups = args.at(-1); + const groups = normalizeRegexGroups(args.at(-1)); if (groups.language === "html") { // html file will be generated for html code blocks without a referenced file - const examplePath = groups.examplePath ? `${groups.examplePath}.html` : `${groups.exampleName}.html`; + const examplePath = groups.examplePath ? groups.examplePath : `${groups.exampleName}.html`; const markupUrl = join("../components", componentPath, examplePath); - replacement += `` + replacement += `` } - if (groups.content && !groups.hidden) { + if (groups.content && !groups.params.hidden) { // add the css/js code blocks for the example. make sure it is indented the way `marked` can handle it replacement += `
@@ -197,7 +251,11 @@ function cleanMarkdownFromExecutableCodeBlocks(markdownContent, name, componentP } return replacement; - }); + } + + return markdownContent + .replace(legacyRegexExecutableCodeBlocks, replacer) + .replace(regexExecutableCodeBlocks, replacer); } module.exports = {