From fe3bb8d4a12b194615fd40b1bd5ca6fda0e6dc5b Mon Sep 17 00:00:00 2001 From: mbehzad Date: Fri, 5 Apr 2024 12:51:44 +0200 Subject: [PATCH 1/4] fix(pv-stylemark): fix regex extracting css code block the regexes where expecting always an empty space after .css which is only the case when a hidden attribute is used, it allowed for the info string to come on the next line as the code fence, only allowed ../ but not ./ --- packages/pv-stylemark/tasks/lsg/getLsgData.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/pv-stylemark/tasks/lsg/getLsgData.js b/packages/pv-stylemark/tasks/lsg/getLsgData.js index 4719a0e..ab58bb5 100644 --- a/packages/pv-stylemark/tasks/lsg/getLsgData.js +++ b/packages/pv-stylemark/tasks/lsg/getLsgData.js @@ -8,11 +8,11 @@ const { resolveApp, getAppConfig } = require("../../helper/paths"); const getStylesData = stylesMatch => { const exampleKeys = stylesMatch - .match(/^\s*[a-zA-Z\-\d_]+.css/g) - .map(match => match.replace(/\s/g, "").replace(/.css$/g, "")); + .match(/^ *[\w\-]+\.css/) + .map(match => match.replace(/ /g, "").replace(/\.css$/g, "")); if (exampleKeys.length === 0) return null; - const styleContent = stylesMatch.replace(/^\s*[a-zA-Z\-\d_]+.css\s+(hidden)?\s+/g, "").trim(); + const styleContent = stylesMatch.replace(/^ *[\w\-]+\.css( +hidden)?\s+/g, "").trim(); return { exampleKey: exampleKeys[0], styleContent, @@ -59,14 +59,14 @@ const getLsgDataForPath = async (path, componentsSrc) => { const { attributes: frontmatterData, body: fileContentBody } = frontmatter(fileContent); - const stylesRegex = new RegExp(/```\s*([a-zA-Z\-\d_]+\.css (hidden)?)\s*[^```]+```/g); + const stylesRegex = new RegExp(/``` *[\w\-]+\.css( +hidden)? *\n+[^```]+```/g); const stylesMatches = fileContentBody.match(stylesRegex) || []; const styles = stylesMatches.map(match => match.replace(/```/g, "")); const stylesList = styles.map(getStylesData); - const exampleRegex = new RegExp(/```\s*[a-zA-Z\-\d_]+:(\.\.\/)*[a-zA-Z\-\d_/]+\.[a-z]+\s*```/g); + const exampleRegex = new RegExp(/``` *[\w\-]+:(\.?\.\/)*[\w\-/]+\.[a-z]+\s*\n```/g); const exampleMatches = fileContentBody.match(exampleRegex) || []; const examples = exampleMatches.map(match => match.replace(/```/g, "").replace(/\s/g, "")); From 9afa746f9df49913fcb328296512628678bf4cf9 Mon Sep 17 00:00:00 2001 From: mbehzad Date: Wed, 10 Apr 2024 15:08:04 +0200 Subject: [PATCH 2/4] feat(pv-stylemark): add suport for js and html executable code blocks. support the hidden attribute --- .../tasks/lsg/buildLsgExamples.js | 15 +- packages/pv-stylemark/tasks/lsg/getLsgData.js | 158 +++++++++++++----- .../tasks/templates/lsg-example.hbs | 9 +- .../components/dds-example/dds-example.scss | 16 +- .../ui/components/dds-example/dds-example.ts | 4 +- 5 files changed, 150 insertions(+), 52 deletions(-) diff --git a/packages/pv-stylemark/tasks/lsg/buildLsgExamples.js b/packages/pv-stylemark/tasks/lsg/buildLsgExamples.js index f57a526..23306b8 100644 --- a/packages/pv-stylemark/tasks/lsg/buildLsgExamples.js +++ b/packages/pv-stylemark/tasks/lsg/buildLsgExamples.js @@ -12,11 +12,22 @@ const loadTemplate = async hbsInst => { return hbsInst.compile(templateContent); }; +/** + * @param {Object} config + * @param {import("./getLsgData.js").StyleMarkLSGData} lsgData + * @param {import("./getLsgData.js").StyleMarkExampleData} exampleData + * @param {Function} template + */ const buildComponentExample = async (config, lsgData, exampleData, template) => { const { destPath } = getAppConfig(); - const componentPath = resolveApp(join(destPath, "components", lsgData.componentPath, exampleData.examplePath)); try { - let componentMarkup = await readFile(componentPath, { encoding: "utf-8" }); + let componentMarkup = ""; + if (exampleData.exampleMarkup.examplePath) { + const componentPath = resolveApp(join(destPath, "components", lsgData.componentPath, exampleData.exampleMarkup.examplePath + ".html")); + 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({ diff --git a/packages/pv-stylemark/tasks/lsg/getLsgData.js b/packages/pv-stylemark/tasks/lsg/getLsgData.js index ab58bb5..98b4d36 100644 --- a/packages/pv-stylemark/tasks/lsg/getLsgData.js +++ b/packages/pv-stylemark/tasks/lsg/getLsgData.js @@ -1,30 +1,58 @@ const { readFile } = require("fs-extra"); -const { resolve, parse: pathParse, normalize, relative: relPath, dirname } = require("path"); +const { resolve, parse: pathParse, normalize, relative: relPath, join } = require("path"); const { marked } = require("marked"); const frontmatter = require("front-matter"); const { glob } = require("glob"); const { resolveApp, getAppConfig } = require("../../helper/paths"); -const getStylesData = stylesMatch => { - const exampleKeys = stylesMatch - .match(/^ *[\w\-]+\.css/) - .map(match => match.replace(/ /g, "").replace(/\.css$/g, "")); - if (exampleKeys.length === 0) return null; - - const styleContent = stylesMatch.replace(/^ *[\w\-]+\.css( +hidden)?\s+/g, "").trim(); - return { - exampleKey: exampleKeys[0], - styleContent, - }; -}; - -const getExampleMarkup = (matchingString, name, componentPath) => { - matchingString = matchingString.replace(/```/g, "").replace(/\s/g, ""); - const [exampleName, examplePath] = matchingString.split(":"); - const markupUrl = `../components/${componentPath}/${examplePath}`; - return ``; -}; +/** + * Information extracted from the executable code blocks according to the stylemark spec (@see https://github.com/mpetrovich/stylemark/blob/main/README-SPEC.md) + * @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 {"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 + * @example + * ```exampleName:examplePath.lang hidden + * content + * ``` + */ + +/** + * @typedef {{ + * exampleName: string; + * exampleMarkup: StyleMarkCodeBlock; + * exampleStyles: StyleMarkCodeBlock[]; + * exampleScripts: StyleMarkCodeBlock[]; + * }} StyleMarkExampleData + */ + +/** + * @typedef {{ + * componentName: string; + * componentPath: string; + * options: Object; + * description: string; + * examples: Array; + * }} StyleMarkLSGData + */ + +// example code blocks +// ```example:/path/to/page.html +// ``` +// +// ```example.js +// console.log('Example 1: ' + data); +// ``` +// +// ```example.css hidden +// button { +// display: none; +// } +// ``` +const regexExecutableCodeBlocks = /``` *(?[\w\-]+)(:(?(\.?\.\/)*[\w\-/]+))?\.(?html|css|js)(?( hidden)?) *\n+(?[^```]*)```/g; const exampleParser = { name: "exampleParser", @@ -51,36 +79,36 @@ const exampleParser = { }, }; -const getLsgDataForPath = async (path, componentsSrc) => { - const fileContent = await readFile(path, { encoding: "utf-8" }); +/** + * read markdown, extract code blocks for the individual examples + * @param {string} markdownPath + * @returns {StyleMarkLSGData} + */ +const getLsgDataForPath = async (markdownPath) => { + const fileContent = await readFile(markdownPath, { encoding: "utf-8" }); - const { name } = pathParse(path); - const componentPath = dirname(relPath(resolveApp(componentsSrc), path)); + const { name, dir } = pathParse(markdownPath); + const componentsSrc = resolveApp(getAppConfig().componentsSrc); + const componentPath = relPath(componentsSrc, dir); const { attributes: frontmatterData, body: fileContentBody } = frontmatter(fileContent); - const stylesRegex = new RegExp(/``` *[\w\-]+\.css( +hidden)? *\n+[^```]+```/g); - - const stylesMatches = fileContentBody.match(stylesRegex) || []; - - const styles = stylesMatches.map(match => match.replace(/```/g, "")); - const stylesList = styles.map(getStylesData); - - const exampleRegex = new RegExp(/``` *[\w\-]+:(\.?\.\/)*[\w\-/]+\.[a-z]+\s*\n```/g); + const codeBlocks = await getExecutableCodeBlocks(fileContentBody); - const exampleMatches = fileContentBody.match(exampleRegex) || []; - const examples = exampleMatches.map(match => match.replace(/```/g, "").replace(/\s/g, "")); - const exampleData = examples.map(match => { - const [exampleName, examplePath] = match.split(":"); - const exampleStyles = stylesList.filter(style => style.exampleKey === exampleName); - return { exampleName, examplePath, exampleStyles }; - }); + const exampleNames = codeBlocks.filter(({language}) => language === "html").map(({ exampleName }) => exampleName); + const exampleData = exampleNames.map(name => ({ + exampleName: name, + // assuming only one html (external file or as the content of the fenced code block) is allowed per example + exampleMarkup: codeBlocks.find(({ exampleName, language }) => exampleName === name && language === "html"), + // multiple css/js code blocks are allowed per example + exampleStyles: codeBlocks.filter(({ exampleName, language }) => exampleName === name && language === "css"), + exampleScripts: codeBlocks.filter(({ exampleName, language }) => exampleName === name && language === "js"), + })); - const cleanContent = fileContentBody - .replace(exampleRegex, match => getExampleMarkup(match, name, componentPath)) - .replace(stylesRegex, ""); + const cleanContent = cleanMarkdownFromExecutableCodeBlocks(fileContentBody, name, componentPath); marked.use({ extensions: [exampleParser] }); const description = marked.parse(cleanContent); + return { componentName: name, componentPath, @@ -120,16 +148,58 @@ const getDataSortedByCategory = (lsgData, config) => { }; const getLsgData = async (curGlob, config) => { - const { componentsSrc } = getAppConfig(); const paths = await glob(curGlob, { windowsPathsNoEscape: true, }); const normalizedPaths = paths.map(filePath => normalize(resolve(process.cwd(), filePath))); - const data = await Promise.all(normalizedPaths.map(curPath => getLsgDataForPath(curPath, componentsSrc))); + const data = await Promise.all(normalizedPaths.map(curPath => getLsgDataForPath(curPath))); return getDataSortedByCategory(data, 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} + */ +async function getExecutableCodeBlocks(markdownContent) { + return Array.from(markdownContent.matchAll(regexExecutableCodeBlocks)) + .map(match => match.groups); +} + +/** + * removes all the fenced code blocks that stylemark will use to render the examples, + * but only for the ones referencing an external file or having the `hidden` attribute in the info string + * + * @param {string} markdownContent + * @returns {string} + */ +function cleanMarkdownFromExecutableCodeBlocks(markdownContent, name, componentPath) { + return markdownContent.replace(regexExecutableCodeBlocks, (...args) => { + let replacement = ""; + /** @type {StyleMarkCodeBlock} */ + const groups = 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 markupUrl = join("../components", componentPath, examplePath); + replacement += `` + } + if (groups.content && !groups.hidden) { + // add the css/js code blocks for the example. make sure it is indented the way `marked` can handle it + replacement += ` +
+ ${groups.language} + \n\`\`\`${groups.language}\n${groups.content}\n\`\`\`\n +
`; + } + + return replacement; + }); +} + module.exports = { getLsgData, }; diff --git a/packages/pv-stylemark/tasks/templates/lsg-example.hbs b/packages/pv-stylemark/tasks/templates/lsg-example.hbs index 02239c7..e00e7c0 100644 --- a/packages/pv-stylemark/tasks/templates/lsg-example.hbs +++ b/packages/pv-stylemark/tasks/templates/lsg-example.hbs @@ -5,11 +5,16 @@ {{{lsgConfig.examples.headHtml}}} {{#each exampleStyles}} {{/each}} {{{componentMarkup}}} + {{#each exampleScripts}} + + {{/each}} - \ No newline at end of file + diff --git a/packages/pv-stylemark/ui/components/dds-example/dds-example.scss b/packages/pv-stylemark/ui/components/dds-example/dds-example.scss index 7c85ced..4fa0d57 100644 --- a/packages/pv-stylemark/ui/components/dds-example/dds-example.scss +++ b/packages/pv-stylemark/ui/components/dds-example/dds-example.scss @@ -43,7 +43,8 @@ dds-example { } } - &__html-box-toggle { + &__html-box-toggle, + &__code-box-toggle { display: flex; gap: 8px; align-items: center; @@ -76,12 +77,23 @@ dds-example { border-bottom: 5px solid transparent; border-left: 6px solid $dds-color__black-040; - .dds-state--open & { + .dds-state--open &, + [open] & { transform: rotate(90deg); } } } + &__code-box-toggle { + margin-top: 16px; + + + pre { + margin: 0; + padding: 24px; + background: $dds-color__black-010; + } + } + &__html-box-content { display: none; padding: 24px; diff --git a/packages/pv-stylemark/ui/components/dds-example/dds-example.ts b/packages/pv-stylemark/ui/components/dds-example/dds-example.ts index 0ba55f0..fdbdaf2 100644 --- a/packages/pv-stylemark/ui/components/dds-example/dds-example.ts +++ b/packages/pv-stylemark/ui/components/dds-example/dds-example.ts @@ -54,7 +54,7 @@ class DSExample extends HTMLElement { this.renderComponent(); window.addEventListener('click', () => this.handleWindowClick()); this.viewportObserver = new IntersectionObserver( - (entries) => this.handleViewportChange(entries), + (entries) => this.handleViewportChange(entries), { threshold: 0 } @@ -65,7 +65,7 @@ class DSExample extends HTMLElement { private renderComponent() { this.renderExampleLink(); this.renderExampleBox(); - this.renderHtmlBox(); + if (this.markupUrl) this.renderHtmlBox(); } private renderExampleLink() { From 59deaac005f60d7dbd9cdeac70022e42cc8e96fd Mon Sep 17 00:00:00 2001 From: mbehzad Date: Wed, 10 Apr 2024 18:04:24 +0200 Subject: [PATCH 3/4] feat(pv-stylemark): add info regarding the source of the components styleguide to the new stlymark this will allow in multi-tenant codebases for easier distinguishing between component with the same name --- packages/pv-stylemark/tasks/lsg/getLsgData.js | 3 +++ .../tasks/templates/lsg-component.hbs | 4 ++++ .../components/dds-component/dds-component.scss | 17 +++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/packages/pv-stylemark/tasks/lsg/getLsgData.js b/packages/pv-stylemark/tasks/lsg/getLsgData.js index 98b4d36..9de5781 100644 --- a/packages/pv-stylemark/tasks/lsg/getLsgData.js +++ b/packages/pv-stylemark/tasks/lsg/getLsgData.js @@ -33,6 +33,7 @@ const { resolveApp, getAppConfig } = require("../../helper/paths"); * @typedef {{ * componentName: string; * componentPath: string; + * srcPath: string; * options: Object; * description: string; * examples: Array; @@ -90,6 +91,7 @@ const getLsgDataForPath = async (markdownPath) => { const { name, dir } = pathParse(markdownPath); const componentsSrc = resolveApp(getAppConfig().componentsSrc); const componentPath = relPath(componentsSrc, dir); + const srcPath = relPath(componentsSrc, markdownPath); const { attributes: frontmatterData, body: fileContentBody } = frontmatter(fileContent); @@ -112,6 +114,7 @@ const getLsgDataForPath = async (markdownPath) => { return { componentName: name, componentPath, + srcPath, options: frontmatterData, description, examples: exampleData, diff --git a/packages/pv-stylemark/tasks/templates/lsg-component.hbs b/packages/pv-stylemark/tasks/templates/lsg-component.hbs index 9f82ad4..08a7636 100644 --- a/packages/pv-stylemark/tasks/templates/lsg-component.hbs +++ b/packages/pv-stylemark/tasks/templates/lsg-component.hbs @@ -1,5 +1,9 @@

{{options.name}}

+

+ Source: + {{srcPath}} +

{{{description}}}
diff --git a/packages/pv-stylemark/ui/components/dds-component/dds-component.scss b/packages/pv-stylemark/ui/components/dds-component/dds-component.scss index ea8573b..0c85a57 100644 --- a/packages/pv-stylemark/ui/components/dds-component/dds-component.scss +++ b/packages/pv-stylemark/ui/components/dds-component/dds-component.scss @@ -11,6 +11,23 @@ margin: 0 0 24px; } + &__source { + margin-bottom: 24px; + font-size: 14px; + font-weight: normal; + color: $dds-color__black-040; + } + + &__source-label { + margin-right: 4px; + font-weight: 700; + text-transform: uppercase; + } + + &__source-path { + font-family: Courier, monospace; + } + &__description { h1 { @extend %dds-typo__headline-1; From c5a5e6606949d36815a69f8f648cfff25308aa8a Mon Sep 17 00:00:00 2001 From: mbehzad Date: Mon, 15 Apr 2024 17:19:21 +0200 Subject: [PATCH 4/4] 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 = {