const { DefaultTheme, DefaultThemeRenderContext, JSX } = require('typedoc'); function bind(fn, first) { return (...r) => fn(first, ...r); } /** * Take text `input` and converts it into a string of all arguments suitable for * the `{% compatibility %}` tag. */ function parseCompatibility(input) { return input .split(',') .map((arg) => `'${arg.trim()}'`) .join(', '); } class SassSiteRenderContext extends DefaultThemeRenderContext { // We don't include Typedoc's JS, so the default means of displaying overloads // as multiple togglable definitions within a single member documentation // doesn't work. Instead, we emit each overload as a separate entry with its // own panel. oldMember = this.member; member = bind(function (context, props) { const signatures = props?.signatures; if (signatures && signatures.length > 1) { const element = JSX.createElement( JSX.Fragment, null, ...signatures.map((signature) => { props.signatures = [signature]; return context.oldMember(props); }), ); props.signatures = signatures; return element; } return context.oldMember(props); }, this); // Add compatibility indicators to the beginning of documentation blocks. oldComment = this.comment; comment = bind((context, props) => { if (!props.comment) return; const compatibilityTags = props.comment.blockTags.filter( (tag) => tag.tag === '@compatibility', ); props.comment.removeTags('@compatibility'); const parent = this.oldComment(props); if (!parent) return; parent.children.unshift( ...compatibilityTags.map((compat) => { // Compatibility tags should have a single text block. const text = compat.content[0].text; // The first line is arguments to `{% compatibility %}` tag, anything // after that is the contents of the block. const lineBreak = text.indexOf('\n'); const compatibilityArgs = parseCompatibility( lineBreak === -1 ? text : text.substring(0, lineBreak), ); const rest = lineBreak === -1 ? null : text.substring(lineBreak + 1).trim(); return JSX.createElement(JSX.Raw, { html: `{% compatibility ${compatibilityArgs} %}` + (rest ? context.markdown(rest) : '') + '{% endcompatibility %}', }); }), ); return parent; }, this); // Convert paragraphs that start with **Heads up!** or **Fun fact!** into // proper callouts. oldMarkdown = this.markdown; markdown = bind( (context, text) => context .oldMarkdown(text) .replace( /
Heads up!<\/strong>([^]*?)<\/p>/g,
'{% headsUp %}$1{% endheadsUp %}',
)
.replace(
/ Fun fact!<\/strong>([^]*?)<\/p>/g,
'{% funFact %}$1{% endfunFact %}',
),
this,
);
// Relative URLs don't work well for index pages since they can be rendered at
// different directory levels, so we just convert all URLs to absolute to be
// safe.
oldUrlTo = this.urlTo;
urlTo = bind(function (context, reflection) {
const relative = context.oldUrlTo(reflection);
const absolute = new URL(
relative,
`relative:///documentation/js-api/${context.theme.markedPlugin.location}`,
);
absolute.pathname = absolute.pathname
.replace(/\.html$/, '')
.replace(/\/index$/, '');
return absolute.toString().replace(/^relative:\/\//, '');
}, this);
}
class SassSiteTheme extends DefaultTheme {
getRenderContext(page) {
this.contextCache ??= new SassSiteRenderContext(
this,
page,
this.application.options,
);
return this.contextCache;
}
render(page, template) {
const context = this.getRenderContext(page);
// The default header includes a search bar that we don't want, so we just
// render title on its own.
const breadcrumb = page.model.parent
? `${heading}