mirror of
https://github.com/danog/sass-site.git
synced 2024-11-26 20:14:53 +01:00
310 lines
9.1 KiB
JavaScript
310 lines
9.1 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
const {
|
|
DefaultTheme,
|
|
DefaultThemeRenderContext,
|
|
JSX,
|
|
UrlMapping,
|
|
} = require('typedoc');
|
|
|
|
function bind(fn, first) {
|
|
return (...r) => fn(first, ...r);
|
|
}
|
|
|
|
class SassSiteRenderContext extends DefaultThemeRenderContext {
|
|
// Link to the external documentation of external APIs. Unfortunately, there's
|
|
// no way to use `addUnknownSymbolResolver` for names that don't come from npm
|
|
// packages.
|
|
attemptExternalResolution = function (symbol) {
|
|
if (symbol.escapedName === 'URL') {
|
|
return 'https://developer.mozilla.org/en-US/docs/Web/API/URL';
|
|
} else if (symbol.escapedName === 'Buffer') {
|
|
return 'https://nodejs.org/api/buffer.html';
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// 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);
|
|
|
|
// Make categories visible in the sidebar as well as in the main page's index.
|
|
// Hopefully this will no longer be necessary once TypeStrong/typedoc#1532 is
|
|
// implemented.
|
|
oldNavigation = this.navigation;
|
|
navigation = bind(function (context, props) {
|
|
const navigation = context.oldNavigation(props);
|
|
const childrenByCategories = context._groupByCategory(props.model);
|
|
if (childrenByCategories.size === 0) return navigation;
|
|
|
|
const secondary = context._getNthChild(navigation, 1);
|
|
if (!secondary) return navigation;
|
|
|
|
const firstLI = context._getNthChild(context._getNthChild(secondary, 0), 0);
|
|
const ul = firstLI.props['class'].startsWith('current ')
|
|
? context._getNthChild(firstLI, 1)
|
|
: context._getNthChild(secondary, 0);
|
|
|
|
ul.children = Array.from(childrenByCategories).map(([title, children]) =>
|
|
JSX.createElement(
|
|
JSX.Fragment,
|
|
null,
|
|
JSX.createElement(
|
|
'li',
|
|
{ class: 'sl-tsd-category-label' },
|
|
JSX.createElement('span', null, title),
|
|
),
|
|
...children.map((child) =>
|
|
JSX.createElement(
|
|
'li',
|
|
{ class: child.cssClasses },
|
|
JSX.createElement(
|
|
'a',
|
|
{
|
|
href: context.urlTo(child),
|
|
class: 'tsd-kind-icon',
|
|
},
|
|
child.name,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
return navigation;
|
|
}, this);
|
|
|
|
// Returns the `n`-th child of a JSX node. For some reason, JSX nodes created
|
|
// by TypeDoc can contain nested arrays, so this traverses them.
|
|
_getNthChild = (node, n) => {
|
|
let i = 0;
|
|
|
|
function traverse(array) {
|
|
for (const element of array) {
|
|
if (element instanceof Array) {
|
|
const result = traverse(element);
|
|
if (result != undefined) return result;
|
|
} else {
|
|
if (i === n) return element;
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return traverse(node.children);
|
|
};
|
|
|
|
// Returns a map from category titles to the set of members of those
|
|
// categories.
|
|
_groupByCategory = (model) => {
|
|
const map = new Map();
|
|
function addCategoriesToMap(categories) {
|
|
for (const category of categories) {
|
|
const children = map.get(category.title);
|
|
if (children) {
|
|
children.push(...category.children);
|
|
} else {
|
|
map.set(category.title, [...category.children]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (model.categories) {
|
|
addCategoriesToMap(model.categories);
|
|
} else if (model.groups) {
|
|
for (const group of model.groups) {
|
|
if (group.categories) addCategoriesToMap(group.categories);
|
|
}
|
|
}
|
|
|
|
return map;
|
|
};
|
|
|
|
// Add compatibility indicators to the beginning of documentation blocks.
|
|
oldComment = this.comment;
|
|
comment = bind((context, props) => {
|
|
const compatibilityTags = props.comment?.tags.filter(
|
|
(tag) => tag.tagName === 'compatibility',
|
|
);
|
|
props.comment?.removeTags('compatibility');
|
|
|
|
const parent = this.oldComment(props);
|
|
if (!parent) return;
|
|
|
|
parent.children.unshift(
|
|
...compatibilityTags.map((compat) => {
|
|
// The first line is arguments to impl_status, anything after that is the
|
|
// contents of the block.
|
|
const lineBreak = compat.text.indexOf('\n');
|
|
const firstLine =
|
|
lineBreak === -1 ? compat.text : compat.text.substring(0, lineBreak);
|
|
const rest =
|
|
lineBreak === -1 ? null : compat.text.substring(lineBreak + 1).trim();
|
|
|
|
return JSX.createElement(JSX.Raw, {
|
|
html:
|
|
`<% impl_status(${firstLine}) ${rest ? 'do' : ''} %>` +
|
|
context.markdown(rest) +
|
|
(rest ? '<% end %>' : ''),
|
|
});
|
|
}),
|
|
);
|
|
|
|
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(
|
|
/<p><strong>Heads up!<\/strong>([^]*?)<\/p>/g,
|
|
'<% heads_up do %>$1<% end %>',
|
|
)
|
|
.replace(
|
|
/<p><strong>Fun fact!<\/strong>([^]*?)<\/p>/g,
|
|
'<% fun_fact do %>$1<% end %>',
|
|
),
|
|
this,
|
|
);
|
|
}
|
|
|
|
class SassSiteTheme extends DefaultTheme {
|
|
constructor(renderer) {
|
|
super(renderer);
|
|
|
|
// This is an ugly monkeypatch but it's necessary to work around
|
|
// TypeStrong/typedoc#1731.
|
|
//
|
|
// 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.
|
|
const ContextAwareRendererComponent = Object.getPrototypeOf(
|
|
this.markedPlugin.constructor,
|
|
);
|
|
const oldGetRelativeUrl =
|
|
ContextAwareRendererComponent.prototype.getRelativeUrl;
|
|
ContextAwareRendererComponent.prototype.getRelativeUrl = function (url) {
|
|
const relative = oldGetRelativeUrl.call(this, url);
|
|
|
|
const absolute = new URL(
|
|
relative,
|
|
`relative:///documentation/js-api/${this.location}`,
|
|
);
|
|
absolute.pathname = absolute.pathname
|
|
.replace(/\.html$/, '')
|
|
.replace(/\/index$/, '');
|
|
return absolute.toString().replace(/^relative:\/\//, '');
|
|
};
|
|
}
|
|
|
|
getRenderContext() {
|
|
this.contextCache ??= new SassSiteRenderContext(
|
|
this,
|
|
this.application.options,
|
|
);
|
|
return this.contextCache;
|
|
}
|
|
|
|
render(page) {
|
|
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
|
|
? `<ul class="tsd-breadcrumb">${JSX.renderElement(
|
|
context.breadcrumb(page.model),
|
|
)}</ul>`
|
|
: '';
|
|
const heading =
|
|
(page.model.kindString === 'Project'
|
|
? ''
|
|
: `${page.model.kindString || ''} `) +
|
|
page.model.name +
|
|
(page.model.typeParameters
|
|
? `<${page.model.typeParameters
|
|
.map((item) => item.name)
|
|
.join(', ')}>`
|
|
: '');
|
|
|
|
// The default template renders a full HTML document, but for the Sass site
|
|
// we want it to be a page within the normal layout so we just render the
|
|
// contents with a header that provides the title tag. We also wrap it in a
|
|
// div with class "typedoc" so we can isolate the typedoc-specific CSS.
|
|
return `---
|
|
title: ${JSON.stringify(`${page.model.name} | JS API`)}
|
|
---
|
|
|
|
<div class="typedoc">
|
|
<header>
|
|
<div class="tsd-page-title">
|
|
<div class="container">
|
|
${breadcrumb}
|
|
<h1>${heading}</h1>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="container container-main">
|
|
<div class="row">
|
|
<div class="col-8 col-content">
|
|
${JSX.renderElement(page.template(page))}
|
|
</div>
|
|
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
|
|
${JSX.renderElement(context.navigation(page))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
getUrls(project) {
|
|
return super
|
|
.getUrls(project)
|
|
.map(
|
|
(mapping) =>
|
|
new UrlMapping(`${mapping.url}.erb`, mapping.model, mapping.template),
|
|
);
|
|
}
|
|
}
|
|
|
|
// TODO: See if there's a graceful way to support "Heads up!" and Compatibility
|
|
// annotations as @-tags rather than needing to write out the HTML by hand.
|
|
|
|
exports.load = (app) => {
|
|
app.renderer.addUnknownSymbolResolver(
|
|
'immutable',
|
|
(name) => `https://immutable-js.com/docs/latest@main/${name}/`,
|
|
);
|
|
app.renderer.addUnknownSymbolResolver('source-map-js', (name) => {
|
|
if (name === 'RawSourceMap') {
|
|
return 'https://github.com/mozilla/source-map/blob/58819f09018d56ef84dc41ba9c93f554e0645169/source-map.d.ts#L15-L23';
|
|
}
|
|
});
|
|
|
|
app.renderer.defineTheme('sass-site', SassSiteTheme);
|
|
};
|