diff --git a/kit/preprocessors/docstring.js b/kit/preprocessors/docstring.js index fe61569c..5d03081b 100644 --- a/kit/preprocessors/docstring.js +++ b/kit/preprocessors/docstring.js @@ -3,46 +3,50 @@ import htmlparser2 from "htmlparser2"; import { replaceAsync, renderSvelteChars, generateTagRegex } from "./utils.js"; import { mdsvexPreprocess } from "./mdsvex/index.js"; -// Preprocessor that converts markdown into Docstring -// svelte component using mdsvexPreprocess +const REGEX_PARAMSDESC = generateTagRegex("paramsdesc"); +const REGEX_PARAMSGROUP = generateTagRegex("paramsgroup", true); +const REGEX_PARAMSGROUP_TITLE = generateTagRegex("paramsgrouptitle"); +const REGEX_PARAMSGROUP_DESC = generateTagRegex("paramsgroupdesc"); +const REGEX_RETDESC = generateTagRegex("retdesc"); +const REGEX_RETTYPE = generateTagRegex("rettype"); +// note: fixes a long-standing tag mismatch (python emits `yielddesc`, the old regex +// matched `yieldesc`), which silently dropped yield descriptions +const REGEX_YIELDESC = generateTagRegex("yielddesc"); +const REGEX_YIELDTYPE = generateTagRegex("yieldtype"); +const REGEX_RAISEDESC = generateTagRegex("raises"); +const REGEX_RAISETYPE = generateTagRegex("raisederrors"); +const REGEX_TIP = /(((?!).)*)<\/Tip>/gms; +const REGEX_CHANGED = + /<(Added|Changed|Deprecated) version="([0-9.v]+)" ?\/?>((((?!<(Added|Changed|Deprecated) version="([0-9.v]+)"\/?>).)*)<\/(Added|Changed|Deprecated)>)?/gms; + +/** + * Python (doc_builder/autodoc.py) emits the `` component + * directly; its body carries the markdown-bearing sections (parameter descriptions, + * return/yield/raise types & descriptions), which must be rendered with the same mdsvex + * pipeline as the rest of the page. This preprocessor renders those sections into the + * remaining component props and closes the component. + */ export const docstringPreprocess = { markup: async ({ content, filename }) => { - const REGEX_DOCSTRING = generateTagRegex("docstring", true); - const REGEX_NAME = generateTagRegex("name"); - const REGEX_ANCHOR = generateTagRegex("anchor"); - const REGEX_SIGNATURE = generateTagRegex("parameters"); - const REGEX_PARAMSDESC = generateTagRegex("paramsdesc"); - const REGEX_PARAMSGROUPS = generateTagRegex("paramgroups"); - const REGEX_RETDESC = generateTagRegex("retdesc"); - const REGEX_RETTYPE = generateTagRegex("rettype"); - const REGEX_YIELDESC = generateTagRegex("yieldesc"); - const REGEX_YIELDTYPE = generateTagRegex("yieldtype"); - const REGEX_RAISEDESC = generateTagRegex("raises"); - const REGEX_RAISETYPE = generateTagRegex("raisederrors"); - const REGEX_SOURCE = generateTagRegex("source"); - const REGEX_TIP = /(((?!).)*)<\/Tip>/gms; - const REGEX_CHANGED = - /<(Added|Changed|Deprecated) version="([0-9.v]+)" ?\/?>((((?!<(Added|Changed|Deprecated) version="([0-9.v]+)"\/?>).)*)<\/(Added|Changed|Deprecated)>)?/gms; - const REGEX_IS_GETSET_DESC = //ms; - - content = await replaceAsync(content, REGEX_DOCSTRING, async (_, docstringBody) => { - docstringBody = renderSvelteChars(docstringBody); - - const name = docstringBody.match(REGEX_NAME)[1]; - const anchor = docstringBody.match(REGEX_ANCHOR)[1]; - const signature = docstringBody.match(REGEX_SIGNATURE)[1]; - - let svelteComponent = `` followed by a newline: attribute values are + // single-line JSON, which may contain `>` inside strings but never a raw newline. + const REGEX_DOCSTRING = /\n)[\s\S])*)>\n([\s\S]*?)<\/Docstring>/g; + + content = await replaceAsync(content, REGEX_DOCSTRING, async (_, metadataAttrs, body) => { + body = renderSvelteChars(body); + + // parameter anchors are prefixed with the object's anchor + const anchor = metadataAttrs.match(/ anchor=\{"((?:[^"\\]|\\.)*)"\}/)?.[1] ?? ""; + + let svelteComponent = ` components that are inside parameter descriptions - code = code.replace(REGEX_TIP, (_, isWarning, tipContent) => { + code = code.replace(REGEX_TIP, (_tip, isWarning, tipContent) => { const color = isWarning ? "orange" : "green"; return `
{ + const color = /Added|Changed/.test(componentType) ? "green" : "orange"; + if (!descriptionContent) { + descriptionContent = ""; + } + return `
${componentType} in ${version}

${descriptionContent}
`; - }); - - const dom = htmlparser2.parseDocument(code); - const lists = domUtils.getElementsByTagName("ul", dom); - if (lists.length) { - const list = lists[0]; - const result = []; - for (const childEl of list.childNodes.filter(({ type }) => type === "tag")) { - const nameEl = domUtils.getElementsByTagName("strong", childEl)[0]; - const name = domUtils.innerText(nameEl); - const paramAnchor = `${anchor}.${name}`; - let description = domUtils.getInnerHTML(childEl).trim(); - - // strip enclosing paragraph tags

&

- if (description.startsWith("

")) { - description = description.slice("

".length); - } - if (description.endsWith("

")) { - description = description.slice(0, -"

".length); - } - - result.push({ anchor: paramAnchor, description, name }); } + ); + + const result = parseRenderedParamsList(code, anchor); + if (result) { svelteComponent += ` parametersDescription={${JSON.stringify(result)}} `; } } - if (docstringBody.match(REGEX_SOURCE)) { - const source = docstringBody.match(REGEX_SOURCE)[1]; - svelteComponent += ` source={${JSON.stringify(source)}} `; - } - - if (docstringBody.match(REGEX_RETDESC)) { - const retDesc = docstringBody.match(REGEX_RETDESC)[1]; + if (body.match(REGEX_RETDESC)) { + const retDesc = body.match(REGEX_RETDESC)[1]; const { code } = await mdsvexPreprocess.markup({ content: retDesc, filename }); svelteComponent += ` returnDescription={${JSON.stringify(code)}} `; } - if (docstringBody.match(REGEX_RETTYPE)) { - const retType = docstringBody.match(REGEX_RETTYPE)[1]; + if (body.match(REGEX_RETTYPE)) { + const retType = body.match(REGEX_RETTYPE)[1]; const { code } = await mdsvexPreprocess.markup({ content: retType, filename }); svelteComponent += ` returnType={${JSON.stringify(code)}} `; } - if (docstringBody.match(REGEX_YIELDESC)) { - const yieldDesc = docstringBody.match(REGEX_YIELDESC)[1]; + if (body.match(REGEX_YIELDESC)) { + const yieldDesc = body.match(REGEX_YIELDESC)[1]; const { code } = await mdsvexPreprocess.markup({ content: yieldDesc, filename }); svelteComponent += ` returnDescription={${JSON.stringify(code)}} `; } - if (docstringBody.match(REGEX_YIELDTYPE)) { - const yieldType = docstringBody.match(REGEX_YIELDTYPE)[1]; + if (body.match(REGEX_YIELDTYPE)) { + const yieldType = body.match(REGEX_YIELDTYPE)[1]; const { code } = await mdsvexPreprocess.markup({ content: yieldType, filename }); svelteComponent += ` returnType={${JSON.stringify(code)}} isYield={true} `; } - if (docstringBody.match(REGEX_RAISEDESC)) { - const raiseDesc = docstringBody.match(REGEX_RAISEDESC)[1]; + if (body.match(REGEX_RAISEDESC)) { + const raiseDesc = body.match(REGEX_RAISEDESC)[1]; const { code } = await mdsvexPreprocess.markup({ content: raiseDesc, filename }); svelteComponent += ` raiseDescription={${JSON.stringify(code)}} `; } - if (docstringBody.match(REGEX_RAISETYPE)) { - const raiseType = docstringBody.match(REGEX_RAISETYPE)[1]; + if (body.match(REGEX_RAISETYPE)) { + const raiseType = body.match(REGEX_RAISETYPE)[1]; const { code } = await mdsvexPreprocess.markup({ content: raiseType, filename }); svelteComponent += ` raiseType={${JSON.stringify(code)}} `; } - if (docstringBody.match(REGEX_IS_GETSET_DESC)) { - svelteComponent += ` isGetSetDescriptor={true} `; + const parameterGroups = []; + for (const [, groupBody] of body.matchAll(REGEX_PARAMSGROUP)) { + const title = groupBody.match(REGEX_PARAMSGROUP_TITLE)[1]; + const groupContent = groupBody.match(REGEX_PARAMSGROUP_DESC)[1]; + const { code } = await mdsvexPreprocess.markup({ content: groupContent, filename }); + parameterGroups.push({ + title, + parametersDescription: parseRenderedParamsList(code, anchor) ?? [], + }); } - - if (docstringBody.match(REGEX_PARAMSGROUPS)) { - const nParamGroups = parseInt(docstringBody.match(REGEX_PARAMSGROUPS)[1]); - if (nParamGroups > 0) { - const parameterGroups = []; - for (let groupId = 1; groupId <= nParamGroups; groupId++) { - const REGEX_GROUP_TITLE = new RegExp( - `(((?!).)*)`, - "ms" - ); - const REGEX_GROUP_CONTENT = new RegExp( - `(((?!).)*)`, - "ms" - ); - const title = docstringBody.match(REGEX_GROUP_TITLE)[1]; - const content = docstringBody.match(REGEX_GROUP_CONTENT)[1]; - const { code } = await mdsvexPreprocess.markup({ content, filename }); - const dom = htmlparser2.parseDocument(code); - const lists = domUtils.getElementsByTagName("ul", dom); - const result = []; - if (lists.length) { - const list = lists[0]; - for (const childEl of list.childNodes.filter(({ type }) => type === "tag")) { - const nameEl = domUtils.getElementsByTagName("strong", childEl)[0]; - const name = domUtils.innerText(nameEl); - const paramAnchor = `${anchor}.${name}`; - let description = domUtils.getInnerHTML(childEl).trim(); - - // strip enclosing paragraph tags

&

- if (description.startsWith("

")) { - description = description.slice("

".length); - } - if (description.endsWith("

")) { - description = description.slice(0, -"

".length); - } - - result.push({ anchor: paramAnchor, description, name }); - } - } - parameterGroups.push({ title, parametersDescription: result }); - } - svelteComponent += ` parameterGroups={${JSON.stringify(parameterGroups)}} `; - } + if (parameterGroups.length) { + svelteComponent += ` parameterGroups={${JSON.stringify(parameterGroups)}} `; } svelteComponent += ` />\n`; @@ -191,8 +140,35 @@ export const docstringPreprocess = { }; /** - * The mdx file contains unnecessarily escaped underscores in the docstring's name + * The parameter descriptions are authored as a markdown bullet list (`- **name** -- desc`) + * and must be rendered as one list to keep exact markdown semantics (tight/loose lists); + * this parses the rendered `
    ` back into per-parameter structured data for the + * `Docstring` component (tooltips, per-parameter anchors). + * @param {string} code rendered html + * @param {string} anchor the object's anchor, used as prefix for parameter anchors */ -function unescapeUnderscores(content) { - return content.replace(/\\_/g, "_"); +function parseRenderedParamsList(code, anchor) { + const dom = htmlparser2.parseDocument(code); + const lists = domUtils.getElementsByTagName("ul", dom); + if (!lists.length) { + return undefined; + } + const result = []; + for (const childEl of lists[0].childNodes.filter(({ type }) => type === "tag")) { + const nameEl = domUtils.getElementsByTagName("strong", childEl)[0]; + const name = domUtils.innerText(nameEl); + const paramAnchor = `${anchor}.${name}`; + let description = domUtils.getInnerHTML(childEl).trim(); + + // strip enclosing paragraph tags

    &

    + if (description.startsWith("

    ")) { + description = description.slice("

    ".length); + } + if (description.endsWith("

    ")) { + description = description.slice(0, -"

    ".length); + } + + result.push({ anchor: paramAnchor, description, name }); + } + return result; } diff --git a/kit/svelte.config.js b/kit/svelte.config.js index 5ae59234..37bdbfeb 100644 --- a/kit/svelte.config.js +++ b/kit/svelte.config.js @@ -55,7 +55,10 @@ const config = { warning.code?.startsWith("a11y") || warning.message.includes("A11y") || // md-generated doc content is full of `