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}}
-