@@ -3,46 +3,50 @@ import htmlparser2 from "htmlparser2";
33import { replaceAsync , renderSvelteChars , generateTagRegex } from "./utils.js" ;
44import { mdsvexPreprocess } from "./mdsvex/index.js" ;
55
6- // Preprocessor that converts markdown into Docstring
7- // svelte component using mdsvexPreprocess
6+ const REGEX_PARAMSDESC = generateTagRegex ( "paramsdesc" ) ;
7+ const REGEX_PARAMSGROUP = generateTagRegex ( "paramsgroup" , true ) ;
8+ const REGEX_PARAMSGROUP_TITLE = generateTagRegex ( "paramsgrouptitle" ) ;
9+ const REGEX_PARAMSGROUP_DESC = generateTagRegex ( "paramsgroupdesc" ) ;
10+ const REGEX_RETDESC = generateTagRegex ( "retdesc" ) ;
11+ const REGEX_RETTYPE = generateTagRegex ( "rettype" ) ;
12+ // note: fixes a long-standing tag mismatch (python emits `yielddesc`, the old regex
13+ // matched `yieldesc`), which silently dropped yield descriptions
14+ const REGEX_YIELDESC = generateTagRegex ( "yielddesc" ) ;
15+ const REGEX_YIELDTYPE = generateTagRegex ( "yieldtype" ) ;
16+ const REGEX_RAISEDESC = generateTagRegex ( "raises" ) ;
17+ const REGEX_RAISETYPE = generateTagRegex ( "raisederrors" ) ;
18+ const REGEX_TIP = / < T i p ( w a r n i n g = { true} ) ? > ( ( (? ! < T i p ( w a r n i n g = { true} ) ? > ) .) * ) < \/ T i p > / gms;
19+ const REGEX_CHANGED =
20+ / < ( A d d e d | C h a n g e d | D e p r e c a t e d ) v e r s i o n = " ( [ 0 - 9 . v ] + ) " ? \/ ? > ( ( ( (? ! < ( A d d e d | C h a n g e d | D e p r e c a t e d ) v e r s i o n = " ( [ 0 - 9 . v ] + ) " \/ ? > ) .) * ) < \/ ( A d d e d | C h a n g e d | D e p r e c a t e d ) > ) ? / gms;
21+
22+ /**
23+ * Python (doc_builder/autodoc.py) emits the `<Docstring ...metadata props...>` component
24+ * directly; its body carries the markdown-bearing sections (parameter descriptions,
25+ * return/yield/raise types & descriptions), which must be rendered with the same mdsvex
26+ * pipeline as the rest of the page. This preprocessor renders those sections into the
27+ * remaining component props and closes the component.
28+ */
829export const docstringPreprocess = {
930 markup : async ( { content, filename } ) => {
10- const REGEX_DOCSTRING = generateTagRegex ( "docstring" , true ) ;
11- const REGEX_NAME = generateTagRegex ( "name" ) ;
12- const REGEX_ANCHOR = generateTagRegex ( "anchor" ) ;
13- const REGEX_SIGNATURE = generateTagRegex ( "parameters" ) ;
14- const REGEX_PARAMSDESC = generateTagRegex ( "paramsdesc" ) ;
15- const REGEX_PARAMSGROUPS = generateTagRegex ( "paramgroups" ) ;
16- const REGEX_RETDESC = generateTagRegex ( "retdesc" ) ;
17- const REGEX_RETTYPE = generateTagRegex ( "rettype" ) ;
18- const REGEX_YIELDESC = generateTagRegex ( "yieldesc" ) ;
19- const REGEX_YIELDTYPE = generateTagRegex ( "yieldtype" ) ;
20- const REGEX_RAISEDESC = generateTagRegex ( "raises" ) ;
21- const REGEX_RAISETYPE = generateTagRegex ( "raisederrors" ) ;
22- const REGEX_SOURCE = generateTagRegex ( "source" ) ;
23- const REGEX_TIP = / < T i p ( w a r n i n g = { true} ) ? > ( ( (? ! < T i p ( w a r n i n g = { true} ) ? > ) .) * ) < \/ T i p > / gms;
24- const REGEX_CHANGED =
25- / < ( A d d e d | C h a n g e d | D e p r e c a t e d ) v e r s i o n = " ( [ 0 - 9 . v ] + ) " ? \/ ? > ( ( ( (? ! < ( A d d e d | C h a n g e d | D e p r e c a t e d ) v e r s i o n = " ( [ 0 - 9 . v ] + ) " \/ ? > ) .) * ) < \/ ( A d d e d | C h a n g e d | D e p r e c a t e d ) > ) ? / gms;
26- const REGEX_IS_GETSET_DESC = / < i s g e t s e t d e s c r i p t o r > / ms;
27-
28- content = await replaceAsync ( content , REGEX_DOCSTRING , async ( _ , docstringBody ) => {
29- docstringBody = renderSvelteChars ( docstringBody ) ;
30-
31- const name = docstringBody . match ( REGEX_NAME ) [ 1 ] ;
32- const anchor = docstringBody . match ( REGEX_ANCHOR ) [ 1 ] ;
33- const signature = docstringBody . match ( REGEX_SIGNATURE ) [ 1 ] ;
34-
35- let svelteComponent = `<Docstring name={${ JSON . stringify (
36- unescapeUnderscores ( name )
37- ) } } anchor={${ JSON . stringify ( anchor ) } } parameters={${ signature } } `;
38-
39- if ( docstringBody . match ( REGEX_PARAMSDESC ) ) {
40- let content = docstringBody . match ( REGEX_PARAMSDESC ) [ 1 ] ;
31+ // The open tag ends at the first `>` followed by a newline: attribute values are
32+ // single-line JSON, which may contain `>` inside strings but never a raw newline.
33+ const REGEX_DOCSTRING = / < D o c s t r i n g ( (?: (? ! > \n ) [ \s \S ] ) * ) > \n ( [ \s \S ] * ?) < \/ D o c s t r i n g > / g;
34+
35+ content = await replaceAsync ( content , REGEX_DOCSTRING , async ( _ , metadataAttrs , body ) => {
36+ body = renderSvelteChars ( body ) ;
37+
38+ // parameter anchors are prefixed with the object's anchor
39+ const anchor = metadataAttrs . match ( / a n c h o r = \{ " ( (?: [ ^ " \\ ] | \\ .) * ) " \} / ) ?. [ 1 ] ?? "" ;
40+
41+ let svelteComponent = `<Docstring${ metadataAttrs } ` ;
42+
43+ if ( body . match ( REGEX_PARAMSDESC ) ) {
44+ let paramsContent = body . match ( REGEX_PARAMSDESC ) [ 1 ] ;
4145 // escape }} by adding void character `‌` in between
42- content = content . replace ( / } } / g, "}‌}" ) ;
43- let { code } = await mdsvexPreprocess . markup ( { content, filename } ) ;
46+ paramsContent = paramsContent . replace ( / } } / g, "}‌}" ) ;
47+ let { code } = await mdsvexPreprocess . markup ( { content : paramsContent , filename } ) ;
4448 // render <Tip> components that are inside parameter descriptions
45- code = code . replace ( REGEX_TIP , ( _ , isWarning , tipContent ) => {
49+ code = code . replace ( REGEX_TIP , ( _tip , isWarning , tipContent ) => {
4650 const color = isWarning ? "orange" : "green" ;
4751 return `<div
4852 class="course-tip ${
@@ -53,133 +57,78 @@ export const docstringPreprocess = {
5357 </div>` ;
5458 } ) ;
5559 // render <Added>, <Changed>, <Deprecated> components that are inside parameter descriptions
56- code = code . replace ( REGEX_CHANGED , ( _ , componentType , version , __ , descriptionContent ) => {
57- const color = / A d d e d | C h a n g e d / . test ( componentType ) ? "green" : "orange" ;
58- if ( ! descriptionContent ) {
59- descriptionContent = "" ;
60- }
61- return `<div
60+ code = code . replace (
61+ REGEX_CHANGED ,
62+ ( _changed , componentType , version , __ , descriptionContent ) => {
63+ const color = / A d d e d | C h a n g e d / . test ( componentType ) ? "green" : "orange" ;
64+ if ( ! descriptionContent ) {
65+ descriptionContent = "" ;
66+ }
67+ return `<div
6268 class="course-tip ${
6369 color === "orange" ? "course-tip-orange" : ""
6470 } bg-gradient-to-br dark:bg-gradient-to-r before:border-${ color } -500 dark:before:border-${ color } -800 from-${ color } -50 dark:from-gray-900 to-white dark:to-gray-950 border border-${ color } -50 text-${ color } -700 dark:text-gray-400"
6571 >
6672 <p class="font-medium">${ componentType } in ${ version } </p>
6773 ${ descriptionContent }
6874 </div>` ;
69- } ) ;
70-
71- const dom = htmlparser2 . parseDocument ( code ) ;
72- const lists = domUtils . getElementsByTagName ( "ul" , dom ) ;
73- if ( lists . length ) {
74- const list = lists [ 0 ] ;
75- const result = [ ] ;
76- for ( const childEl of list . childNodes . filter ( ( { type } ) => type === "tag" ) ) {
77- const nameEl = domUtils . getElementsByTagName ( "strong" , childEl ) [ 0 ] ;
78- const name = domUtils . innerText ( nameEl ) ;
79- const paramAnchor = `${ anchor } .${ name } ` ;
80- let description = domUtils . getInnerHTML ( childEl ) . trim ( ) ;
81-
82- // strip enclosing paragraph tags <p> & </p>
83- if ( description . startsWith ( "<p>" ) ) {
84- description = description . slice ( "<p>" . length ) ;
85- }
86- if ( description . endsWith ( "</p>" ) ) {
87- description = description . slice ( 0 , - "</p>" . length ) ;
88- }
89-
90- result . push ( { anchor : paramAnchor , description, name } ) ;
9175 }
76+ ) ;
77+
78+ const result = parseRenderedParamsList ( code , anchor ) ;
79+ if ( result ) {
9280 svelteComponent += ` parametersDescription={${ JSON . stringify ( result ) } } ` ;
9381 }
9482 }
9583
96- if ( docstringBody . match ( REGEX_SOURCE ) ) {
97- const source = docstringBody . match ( REGEX_SOURCE ) [ 1 ] ;
98- svelteComponent += ` source={${ JSON . stringify ( source ) } } ` ;
99- }
100-
101- if ( docstringBody . match ( REGEX_RETDESC ) ) {
102- const retDesc = docstringBody . match ( REGEX_RETDESC ) [ 1 ] ;
84+ if ( body . match ( REGEX_RETDESC ) ) {
85+ const retDesc = body . match ( REGEX_RETDESC ) [ 1 ] ;
10386 const { code } = await mdsvexPreprocess . markup ( { content : retDesc , filename } ) ;
10487 svelteComponent += ` returnDescription={${ JSON . stringify ( code ) } } ` ;
10588 }
10689
107- if ( docstringBody . match ( REGEX_RETTYPE ) ) {
108- const retType = docstringBody . match ( REGEX_RETTYPE ) [ 1 ] ;
90+ if ( body . match ( REGEX_RETTYPE ) ) {
91+ const retType = body . match ( REGEX_RETTYPE ) [ 1 ] ;
10992 const { code } = await mdsvexPreprocess . markup ( { content : retType , filename } ) ;
11093 svelteComponent += ` returnType={${ JSON . stringify ( code ) } } ` ;
11194 }
11295
113- if ( docstringBody . match ( REGEX_YIELDESC ) ) {
114- const yieldDesc = docstringBody . match ( REGEX_YIELDESC ) [ 1 ] ;
96+ if ( body . match ( REGEX_YIELDESC ) ) {
97+ const yieldDesc = body . match ( REGEX_YIELDESC ) [ 1 ] ;
11598 const { code } = await mdsvexPreprocess . markup ( { content : yieldDesc , filename } ) ;
11699 svelteComponent += ` returnDescription={${ JSON . stringify ( code ) } } ` ;
117100 }
118101
119- if ( docstringBody . match ( REGEX_YIELDTYPE ) ) {
120- const yieldType = docstringBody . match ( REGEX_YIELDTYPE ) [ 1 ] ;
102+ if ( body . match ( REGEX_YIELDTYPE ) ) {
103+ const yieldType = body . match ( REGEX_YIELDTYPE ) [ 1 ] ;
121104 const { code } = await mdsvexPreprocess . markup ( { content : yieldType , filename } ) ;
122105 svelteComponent += ` returnType={${ JSON . stringify ( code ) } } isYield={true} ` ;
123106 }
124107
125- if ( docstringBody . match ( REGEX_RAISEDESC ) ) {
126- const raiseDesc = docstringBody . match ( REGEX_RAISEDESC ) [ 1 ] ;
108+ if ( body . match ( REGEX_RAISEDESC ) ) {
109+ const raiseDesc = body . match ( REGEX_RAISEDESC ) [ 1 ] ;
127110 const { code } = await mdsvexPreprocess . markup ( { content : raiseDesc , filename } ) ;
128111 svelteComponent += ` raiseDescription={${ JSON . stringify ( code ) } } ` ;
129112 }
130113
131- if ( docstringBody . match ( REGEX_RAISETYPE ) ) {
132- const raiseType = docstringBody . match ( REGEX_RAISETYPE ) [ 1 ] ;
114+ if ( body . match ( REGEX_RAISETYPE ) ) {
115+ const raiseType = body . match ( REGEX_RAISETYPE ) [ 1 ] ;
133116 const { code } = await mdsvexPreprocess . markup ( { content : raiseType , filename } ) ;
134117 svelteComponent += ` raiseType={${ JSON . stringify ( code ) } } ` ;
135118 }
136119
137- if ( docstringBody . match ( REGEX_IS_GETSET_DESC ) ) {
138- svelteComponent += ` isGetSetDescriptor={true} ` ;
120+ const parameterGroups = [ ] ;
121+ for ( const [ , groupBody ] of body . matchAll ( REGEX_PARAMSGROUP ) ) {
122+ const title = groupBody . match ( REGEX_PARAMSGROUP_TITLE ) [ 1 ] ;
123+ const groupContent = groupBody . match ( REGEX_PARAMSGROUP_DESC ) [ 1 ] ;
124+ const { code } = await mdsvexPreprocess . markup ( { content : groupContent , filename } ) ;
125+ parameterGroups . push ( {
126+ title,
127+ parametersDescription : parseRenderedParamsList ( code , anchor ) ?? [ ] ,
128+ } ) ;
139129 }
140-
141- if ( docstringBody . match ( REGEX_PARAMSGROUPS ) ) {
142- const nParamGroups = parseInt ( docstringBody . match ( REGEX_PARAMSGROUPS ) [ 1 ] ) ;
143- if ( nParamGroups > 0 ) {
144- const parameterGroups = [ ] ;
145- for ( let groupId = 1 ; groupId <= nParamGroups ; groupId ++ ) {
146- const REGEX_GROUP_TITLE = new RegExp (
147- `<paramsdesc${ groupId } title>(((?!<paramsdesc${ groupId } title>).)*)</paramsdesc${ groupId } title>` ,
148- "ms"
149- ) ;
150- const REGEX_GROUP_CONTENT = new RegExp (
151- `<paramsdesc${ groupId } >(((?!<paramsdesc${ groupId } >).)*)</paramsdesc${ groupId } >` ,
152- "ms"
153- ) ;
154- const title = docstringBody . match ( REGEX_GROUP_TITLE ) [ 1 ] ;
155- const content = docstringBody . match ( REGEX_GROUP_CONTENT ) [ 1 ] ;
156- const { code } = await mdsvexPreprocess . markup ( { content, filename } ) ;
157- const dom = htmlparser2 . parseDocument ( code ) ;
158- const lists = domUtils . getElementsByTagName ( "ul" , dom ) ;
159- const result = [ ] ;
160- if ( lists . length ) {
161- const list = lists [ 0 ] ;
162- for ( const childEl of list . childNodes . filter ( ( { type } ) => type === "tag" ) ) {
163- const nameEl = domUtils . getElementsByTagName ( "strong" , childEl ) [ 0 ] ;
164- const name = domUtils . innerText ( nameEl ) ;
165- const paramAnchor = `${ anchor } .${ name } ` ;
166- let description = domUtils . getInnerHTML ( childEl ) . trim ( ) ;
167-
168- // strip enclosing paragraph tags <p> & </p>
169- if ( description . startsWith ( "<p>" ) ) {
170- description = description . slice ( "<p>" . length ) ;
171- }
172- if ( description . endsWith ( "</p>" ) ) {
173- description = description . slice ( 0 , - "</p>" . length ) ;
174- }
175-
176- result . push ( { anchor : paramAnchor , description, name } ) ;
177- }
178- }
179- parameterGroups . push ( { title, parametersDescription : result } ) ;
180- }
181- svelteComponent += ` parameterGroups={${ JSON . stringify ( parameterGroups ) } } ` ;
182- }
130+ if ( parameterGroups . length ) {
131+ svelteComponent += ` parameterGroups={${ JSON . stringify ( parameterGroups ) } } ` ;
183132 }
184133
185134 svelteComponent += ` />\n` ;
@@ -191,8 +140,35 @@ export const docstringPreprocess = {
191140} ;
192141
193142/**
194- * The mdx file contains unnecessarily escaped underscores in the docstring's name
143+ * The parameter descriptions are authored as a markdown bullet list (`- **name** -- desc`)
144+ * and must be rendered as one list to keep exact markdown semantics (tight/loose lists);
145+ * this parses the rendered `<ul>` back into per-parameter structured data for the
146+ * `Docstring` component (tooltips, per-parameter anchors).
147+ * @param {string } code rendered html
148+ * @param {string } anchor the object's anchor, used as prefix for parameter anchors
195149 */
196- function unescapeUnderscores ( content ) {
197- return content . replace ( / \\ _ / g, "_" ) ;
150+ function parseRenderedParamsList ( code , anchor ) {
151+ const dom = htmlparser2 . parseDocument ( code ) ;
152+ const lists = domUtils . getElementsByTagName ( "ul" , dom ) ;
153+ if ( ! lists . length ) {
154+ return undefined ;
155+ }
156+ const result = [ ] ;
157+ for ( const childEl of lists [ 0 ] . childNodes . filter ( ( { type } ) => type === "tag" ) ) {
158+ const nameEl = domUtils . getElementsByTagName ( "strong" , childEl ) [ 0 ] ;
159+ const name = domUtils . innerText ( nameEl ) ;
160+ const paramAnchor = `${ anchor } .${ name } ` ;
161+ let description = domUtils . getInnerHTML ( childEl ) . trim ( ) ;
162+
163+ // strip enclosing paragraph tags <p> & </p>
164+ if ( description . startsWith ( "<p>" ) ) {
165+ description = description . slice ( "<p>" . length ) ;
166+ }
167+ if ( description . endsWith ( "</p>" ) ) {
168+ description = description . slice ( 0 , - "</p>" . length ) ;
169+ }
170+
171+ result . push ( { anchor : paramAnchor , description, name } ) ;
172+ }
173+ return result ;
198174}
0 commit comments