const { DefaultTheme, DefaultThemeRenderContext, JSX, UrlMapping, // eslint-disable-next-line node/no-missing-require, node/no-extraneous-require } = require('typedoc'); const child_process = require('child_process'); const fs = require('fs'); 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((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 restOfFirst = lineBreak === -1 ? null : text.substring(lineBreak + 1); const rest = [ ...(restOfFirst ? [{kind: 'text', text: restOfFirst}] : []), ...compat.content.slice(1), ]; 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((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;
}
getUrls(project) {
const urls = super.getUrls(project);
// Most pages in the docs have their date set based on their last git
// commit, but that doesn't work for the generated TypeDoc. To avoid having
// a bunch of churn in the sitemap for these files, we set their date to the
// last modified date for the entire directory.
if (fs.existsSync('js-api-doc')) {
const lastModified = child_process
.execSync('git log -1 --date=iso-strict --format=%cd js-api-doc', {
encoding: 'utf8',
})
.trim();
urls.push(
new UrlMapping(
'js-api.11tydata.json',
// This isn't actually based on a TypeDoc model, but if we don't
// provide a real one TypeDoc will crash. See TypeStrong/typedoc#2318.
urls[0].model,
() =>
JSON.stringify({
date: lastModified,
})
)
);
}
return urls;
}
render(page, template) {
// This is js-api.11tydata.json.
if (page.url === 'js-api.11tydata.json') return 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}