Skip to content

Commit

Permalink
feat(pv-stylemark): add suport for js and html executable code blocks…
Browse files Browse the repository at this point in the history
…. support the hidden attribute
  • Loading branch information
mbehzad committed Apr 10, 2024
1 parent fe3bb8d commit 9afa746
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 52 deletions.
15 changes: 13 additions & 2 deletions packages/pv-stylemark/tasks/lsg/buildLsgExamples.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
158 changes: 114 additions & 44 deletions packages/pv-stylemark/tasks/lsg/getLsgData.js
Original file line number Diff line number Diff line change
@@ -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 `<dds-example name="${exampleName}" path="${name}-${exampleName}.html" markup-url="${markupUrl}"></dds-example>`;
};
/**
* 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<StyleMarkExampleData>;
* }} StyleMarkLSGData
*/

// example code blocks
// ```example:/path/to/page.html
// ```
//
// ```example.js
// console.log('Example 1: ' + data);
// ```
//
// ```example.css hidden
// button {
// display: none;
// }
// ```
const regexExecutableCodeBlocks = /``` *(?<exampleName>[\w\-]+)(:(?<examplePath>(\.?\.\/)*[\w\-/]+))?\.(?<language>html|css|js)(?<hidden>( hidden)?) *\n+(?<content>[^```]*)```/g;

const exampleParser = {
name: "exampleParser",
Expand All @@ -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,
Expand Down Expand Up @@ -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<StyleMarkCodeBlock>}
*/
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 += `<dds-example name="${groups.exampleName}" path="${name}-${groups.exampleName}.html" ${groups.examplePath && !groups.hidden ? `markup-url="${markupUrl}"`: ""}></dds-example>`
}
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 += `
<details>
<summary class="dds-example__code-box-toggle">${groups.language}</summary>
\n\`\`\`${groups.language}\n${groups.content}\n\`\`\`\n
</details>`;
}

return replacement;
});
}

module.exports = {
getLsgData,
};
9 changes: 7 additions & 2 deletions packages/pv-stylemark/tasks/templates/lsg-example.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
{{{lsgConfig.examples.headHtml}}}
{{#each exampleStyles}}
<style>
{{this.styleContent}}
{{content}}
</style>
{{/each}}
</head>
<body>
{{{componentMarkup}}}
{{#each exampleScripts}}
<script>
{{content}}
</script>
{{/each}}
</body>
</html>
</html>
16 changes: 14 additions & 2 deletions packages/pv-stylemark/ui/components/dds-example/dds-example.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ dds-example {
}
}

&__html-box-toggle {
&__html-box-toggle,
&__code-box-toggle {
display: flex;
gap: 8px;
align-items: center;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -65,7 +65,7 @@ class DSExample extends HTMLElement {
private renderComponent() {
this.renderExampleLink();
this.renderExampleBox();
this.renderHtmlBox();
if (this.markupUrl) this.renderHtmlBox();
}

private renderExampleLink() {
Expand Down

0 comments on commit 9afa746

Please sign in to comment.