262 lines
9.9 KiB
JavaScript
262 lines
9.9 KiB
JavaScript
import { marked } from 'marked';
|
|
import { escapeAttribute, highlightCode } from './highlight.js';
|
|
|
|
const modules = import.meta.glob('../../../../doc/manual/source/**/*.md', {
|
|
query: '?raw',
|
|
import: 'default',
|
|
eager: true,
|
|
});
|
|
|
|
const documentEntries = Object.entries(modules).map(([path, content]) => {
|
|
const relativePath = path
|
|
.replace(/^\.\.\/\.\.\/\.\.\/\.\.\/doc\/manual\/source\//, '')
|
|
.replace(/\.md$/, '');
|
|
const pathParts = relativePath.split('/');
|
|
const locale = pathParts[0] === 'jp' ? 'ja' : 'en';
|
|
const localizedParts = locale === 'ja' ? pathParts.slice(1) : pathParts;
|
|
const sourcePath = localizedParts.join('/');
|
|
const base = localizedParts.slice(0, -1);
|
|
const slug = sourcePath.replace(/\/index$/, '') || 'index';
|
|
return { locale, slug, content, base };
|
|
});
|
|
|
|
const docsByLocale = Object.fromEntries(
|
|
['en', 'ja'].map((locale) => [
|
|
locale,
|
|
Object.fromEntries(
|
|
documentEntries
|
|
.filter((entry) => entry.locale === locale)
|
|
.map(({ slug, content }) => [slug, content]),
|
|
),
|
|
]),
|
|
);
|
|
|
|
const docBasesByLocale = Object.fromEntries(
|
|
['en', 'ja'].map((locale) => [
|
|
locale,
|
|
Object.fromEntries(
|
|
documentEntries
|
|
.filter((entry) => entry.locale === locale)
|
|
.map(({ slug, base }) => [slug, base]),
|
|
),
|
|
]),
|
|
);
|
|
|
|
const englishNav = [
|
|
{ title: 'Introduction', slug: 'introduction' },
|
|
{
|
|
title: 'Language Specification',
|
|
slug: 'language',
|
|
children: [
|
|
{ title: 'Syntax', slug: 'language/syntax' },
|
|
{ title: 'Grammar', slug: 'language/grammar' },
|
|
{
|
|
title: 'Value',
|
|
slug: 'language/value',
|
|
children: [
|
|
{ title: 'String', slug: 'language/value/string' },
|
|
{ title: 'Int', slug: 'language/value/int' },
|
|
{ title: 'Float', slug: 'language/value/float' },
|
|
{ title: 'Bool', slug: 'language/value/bool' },
|
|
],
|
|
},
|
|
{
|
|
title: 'Expression',
|
|
slug: 'language/expression',
|
|
children: [
|
|
{ title: 'Literal', slug: 'language/expression/literal' },
|
|
{ title: 'Identifier', slug: 'language/expression/identifier' },
|
|
{ title: 'Path Reference', slug: 'language/expression/path-reference' },
|
|
{ title: 'Object', slug: 'language/expression/object' },
|
|
{ title: 'Array', slug: 'language/expression/array' },
|
|
{ title: 'Function', slug: 'language/expression/function' },
|
|
{ title: 'Function Call', slug: 'language/expression/function-call' },
|
|
{ title: 'Let', slug: 'language/expression/let' },
|
|
{ title: 'Match', slug: 'language/expression/match' },
|
|
{ title: 'Import', slug: 'language/expression/import' },
|
|
{ title: 'Composition', slug: 'language/expression/composition' },
|
|
{ title: 'Range Refinement', slug: 'language/expression/ascription' },
|
|
{ title: 'Default', slug: 'language/expression/default' },
|
|
{ title: 'Arithmetic', slug: 'language/expression/arithmetic' },
|
|
{ title: 'Logical and Comparison', slug: 'language/expression/logical-and-comparison' },
|
|
],
|
|
},
|
|
{ title: 'Constraints and Defaults', slug: 'language/constraints-and-defaults' },
|
|
{ title: 'Operators', slug: 'language/operators' },
|
|
{ title: 'Functions', slug: 'language/functions' },
|
|
{ title: 'Modules and Imports', slug: 'language/modules-and-imports' },
|
|
{ title: 'Evaluation Semantics', slug: 'language/evaluation' },
|
|
{ title: 'Materialization and Errors', slug: 'language/materialization-and-errors' },
|
|
{ title: 'Naming', slug: 'language/naming' },
|
|
{ title: 'Examples', slug: 'language/examples' },
|
|
],
|
|
},
|
|
{ title: 'Embedding', slug: 'embedding' },
|
|
{ title: 'Packages and Integrations', slug: 'components' },
|
|
];
|
|
|
|
const japaneseNav = [
|
|
{ title: 'はじめに', slug: 'introduction' },
|
|
{
|
|
title: '言語仕様',
|
|
slug: 'language',
|
|
children: [
|
|
{ title: '構文と字句', slug: 'language/syntax' },
|
|
{ title: '文法', slug: 'language/grammar' },
|
|
{
|
|
title: '値',
|
|
slug: 'language/value',
|
|
children: [
|
|
{ title: 'String', slug: 'language/value/string' },
|
|
{ title: 'Int', slug: 'language/value/int' },
|
|
{ title: 'Float', slug: 'language/value/float' },
|
|
{ title: 'Bool', slug: 'language/value/bool' },
|
|
],
|
|
},
|
|
{
|
|
title: '式',
|
|
slug: 'language/expression',
|
|
children: [
|
|
{ title: 'リテラル', slug: 'language/expression/literal' },
|
|
{ title: '識別子', slug: 'language/expression/identifier' },
|
|
{ title: 'パス参照', slug: 'language/expression/path-reference' },
|
|
{ title: 'オブジェクト', slug: 'language/expression/object' },
|
|
{ title: '配列', slug: 'language/expression/array' },
|
|
{ title: '関数', slug: 'language/expression/function' },
|
|
{ title: '関数呼び出し', slug: 'language/expression/function-call' },
|
|
{ title: 'let', slug: 'language/expression/let' },
|
|
{ title: 'match', slug: 'language/expression/match' },
|
|
{ title: 'import', slug: 'language/expression/import' },
|
|
{ title: '合成', slug: 'language/expression/composition' },
|
|
{ title: '範囲の絞り込み', slug: 'language/expression/ascription' },
|
|
{ title: 'default', slug: 'language/expression/default' },
|
|
{ title: '算術式', slug: 'language/expression/arithmetic' },
|
|
{ title: '論理式と比較式', slug: 'language/expression/logical-and-comparison' },
|
|
],
|
|
},
|
|
{ title: '制約とdefault', slug: 'language/constraints-and-defaults' },
|
|
{ title: '演算子', slug: 'language/operators' },
|
|
{ title: '関数', slug: 'language/functions' },
|
|
{ title: 'モジュールとimport', slug: 'language/modules-and-imports' },
|
|
{ title: '遅延評価', slug: 'language/evaluation' },
|
|
{ title: '具体化とエラー', slug: 'language/materialization-and-errors' },
|
|
{ title: '命名規約', slug: 'language/naming' },
|
|
{ title: '例', slug: 'language/examples' },
|
|
],
|
|
},
|
|
{ title: '組み込み', slug: 'embedding' },
|
|
{ title: 'パッケージと統合', slug: 'components' },
|
|
];
|
|
|
|
const navByLocale = {
|
|
en: englishNav,
|
|
ja: japaneseNav,
|
|
};
|
|
|
|
const renderer = new marked.Renderer();
|
|
|
|
renderer.code = (code, language = '') => {
|
|
const normalizedLanguage = language.split(/\s+/)[0] ?? '';
|
|
const className = normalizedLanguage ? ` class="language-${escapeAttribute(normalizedLanguage)}"` : '';
|
|
return `<pre class="code-block"><code${className}>${highlightCode(code, normalizedLanguage)}</code></pre>`;
|
|
};
|
|
|
|
renderer.codespan = (code) => `<code>${code}</code>`;
|
|
|
|
marked.setOptions({ gfm: true, renderer });
|
|
|
|
export function getNav(locale = 'en') {
|
|
return navByLocale[locale] ?? englishNav;
|
|
}
|
|
|
|
export function allDocSlugs(locale = 'en') {
|
|
return Object.keys(docsByLocale[locale] ?? {}).filter((slug) => slug !== 'index');
|
|
}
|
|
|
|
function flattenNav(items) {
|
|
return items.flatMap((item) => [item.slug, ...(item.children ? flattenNav(item.children) : [])]);
|
|
}
|
|
|
|
export function orderedDocSlugs(locale = 'en', { includeIndex = false } = {}) {
|
|
const localizedDocs = docsByLocale[locale] ?? docsByLocale.en;
|
|
const slugs = flattenNav(getNav(locale)).filter((slug) => localizedDocs[slug]);
|
|
return includeIndex ? ['index', ...slugs] : slugs;
|
|
}
|
|
|
|
function findDocTitle(slug, items) {
|
|
for (const item of items) {
|
|
if (item.slug === slug) return item.title;
|
|
if (item.children) {
|
|
const childTitle = findDocTitle(slug, item.children);
|
|
if (childTitle) return childTitle;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function docTitle(slug, locale = 'en') {
|
|
if (slug === 'index') return locale === 'ja' ? 'Decodalマニュアル' : 'Decodal Manual';
|
|
return findDocTitle(slug, getNav(locale)) ?? slug;
|
|
}
|
|
|
|
function getSource(slug, locale) {
|
|
const localizedDocs = docsByLocale[locale] ?? docsByLocale.en;
|
|
return localizedDocs[slug] ?? localizedDocs.index ?? '# Not found\n';
|
|
}
|
|
|
|
function stripInlineMarkdown(value) {
|
|
return value
|
|
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
.replace(/[`*_~]/g, '')
|
|
.replace(/<[^>]+>/g, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
export function docDescription(slug, locale = 'en') {
|
|
const blocks = getSource(slug, locale)
|
|
.replace(/```[\s\S]*?```/g, '')
|
|
.split(/\n\s*\n/)
|
|
.map((block) => block.trim())
|
|
.filter((block) => block && !block.startsWith('#') && !/^[-*]\s/.test(block));
|
|
const description = stripInlineMarkdown(blocks[0] ?? '');
|
|
if (description.length <= 180) return description;
|
|
const shortened = description.slice(0, 177).replace(/\s+\S*$/, '').trimEnd();
|
|
return `${shortened || description.slice(0, 177)}…`;
|
|
}
|
|
|
|
export function sourceMarkdown(slug, locale = 'en') {
|
|
return getSource(slug, locale).replace(/\]\(([^)\s]+)\)/g, (all, destination) => {
|
|
const [href, hash = ''] = destination.split('#', 2);
|
|
if (!href.endsWith('.md')) return all;
|
|
const target = normalizeDocLink(locale, slug, href.slice(0, -3));
|
|
const pathname = target === 'index'
|
|
? `/docs/${locale}.md`
|
|
: `/docs/${locale}/${target}.md`;
|
|
return `](${pathname}${hash ? `#${hash}` : ''})`;
|
|
});
|
|
}
|
|
|
|
export function renderMarkdown(slug, locale = 'en') {
|
|
const html = marked.parse(getSource(slug, locale));
|
|
return html.replace(/href="([^"#][^"]*)\.md(#[^"]*)?"/g, (_all, href, hash = '') => {
|
|
const target = normalizeDocLink(locale, slug, href);
|
|
const pathname = target === 'index' ? `/docs/${locale}/` : `/docs/${locale}/${target}/`;
|
|
return `href="${pathname}${hash}"`;
|
|
});
|
|
}
|
|
|
|
function normalizeDocLink(locale, currentSlug, href) {
|
|
const base = docBasesByLocale[locale]?.[currentSlug] ?? [];
|
|
const parts = [...base, ...href.split('/')];
|
|
const out = [];
|
|
for (const part of parts) {
|
|
if (!part || part === '.') continue;
|
|
if (part === '..') out.pop();
|
|
else out.push(part);
|
|
}
|
|
if (out[out.length - 1] === 'index') out.pop();
|
|
return out.join('/') || 'index';
|
|
}
|