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 `
${highlightCode(code, normalizedLanguage)}
`; }; renderer.codespan = (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'; }