create-ngmd 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/index.mjs +165 -0
- package/package.json +49 -0
- package/template/README.md +26 -0
- package/template/angular.json +54 -0
- package/template/index.html +47 -0
- package/template/link-guard.plugin.ts +190 -0
- package/template/package.json +76 -0
- package/template/page-meta.plugin.ts +132 -0
- package/template/public/analog.svg +1 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/favicon.svg +46 -0
- package/template/public/logo-mark.svg +46 -0
- package/template/public/logo.svg +53 -0
- package/template/public/vite.svg +1 -0
- package/template/sitemap.plugin.ts +148 -0
- package/template/src/app/app.config.server.ts +10 -0
- package/template/src/app/app.config.ts +59 -0
- package/template/src/app/app.spec.ts +20 -0
- package/template/src/app/app.ts +260 -0
- package/template/src/app/components/breadcrumb.ts +76 -0
- package/template/src/app/components/code-copy.ts +90 -0
- package/template/src/app/components/code-group.ts +70 -0
- package/template/src/app/components/command-palette.ts +299 -0
- package/template/src/app/components/external-links.ts +55 -0
- package/template/src/app/components/heading-anchors.ts +95 -0
- package/template/src/app/components/hlm-card.ts +31 -0
- package/template/src/app/components/media-enhancer.ts +95 -0
- package/template/src/app/components/page-footer.ts +107 -0
- package/template/src/app/components/sidebar.ts +65 -0
- package/template/src/app/components/toc.ts +131 -0
- package/template/src/app/layout-mode.service.ts +10 -0
- package/template/src/app/pages/[...not-found].page.ts +47 -0
- package/template/src/app/pages/index.page.ts +28 -0
- package/template/src/app/pages/welcome.page.ts +18 -0
- package/template/src/app/theme.ts +54 -0
- package/template/src/app/title-strategy.ts +48 -0
- package/template/src/app/ui/alert.ts +46 -0
- package/template/src/app/ui/callout.ts +57 -0
- package/template/src/app/ui/card.ts +41 -0
- package/template/src/app/ui/code-block.ts +76 -0
- package/template/src/app/ui/hero.ts +42 -0
- package/template/src/app/ui/image.ts +26 -0
- package/template/src/app/ui/index.ts +45 -0
- package/template/src/app/ui/pill.ts +42 -0
- package/template/src/app/ui/tabs.ts +76 -0
- package/template/src/app/ui/video.ts +40 -0
- package/template/src/app/ui/workflow.ts +51 -0
- package/template/src/content/welcome.md +19 -0
- package/template/src/main.server.ts +7 -0
- package/template/src/main.ts +6 -0
- package/template/src/marked-extensions/index.ts +47 -0
- package/template/src/marked-extensions/ngmd-code-group.ts +126 -0
- package/template/src/marked-extensions/ngmd-code-highlight.ts +102 -0
- package/template/src/marked-extensions/ngmd-code-import.ts +118 -0
- package/template/src/marked-extensions/ngmd-image.ts +41 -0
- package/template/src/marked-extensions/ngmd-keywords.ts +75 -0
- package/template/src/marked-extensions/ngmd-video.ts +48 -0
- package/template/src/marked-extensions/shiki-shared.ts +42 -0
- package/template/src/ngmd.config.ts +98 -0
- package/template/src/server/routes/api/v1/hello.ts +3 -0
- package/template/src/styles.css +347 -0
- package/template/src/test-setup.ts +6 -0
- package/template/src/vite-env.d.ts +9 -0
- package/template/tsconfig.app.json +14 -0
- package/template/tsconfig.json +34 -0
- package/template/tsconfig.spec.json +11 -0
- package/template/vite.config.ts +78 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import type { MarkedExtension } from 'marked';
|
|
4
|
+
import { getHighlighter, LANGS, escapeHtml } from './shiki-shared';
|
|
5
|
+
import config from '../ngmd.config';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fenced code blocks can import their content from a source file by adding
|
|
9
|
+
* `file="..."` to the info string. Supports GitHub-style `#L5-L20` line
|
|
10
|
+
* ranges so docs reference the *real* code instead of a hand-typed copy
|
|
11
|
+
* that rots out of sync.
|
|
12
|
+
*
|
|
13
|
+
* ```ts file="src/app/hello.ts"
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ```ts file="src/app/hello.ts#L5-L20"
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Renders as a `<div class="ngmd-code-import">` wrapper with a header bar
|
|
20
|
+
* linking to the file on GitHub (via `ngmd.config.ts > site.githubUrl`).
|
|
21
|
+
* Lines marked `// ngmd-ignore-line` are stripped from the imported snippet.
|
|
22
|
+
*
|
|
23
|
+
* Pre-rendered through the shared shiki highlighter so the output is one
|
|
24
|
+
* self-contained HTML block — marked never sees the inner fence.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const FENCE_RE =
|
|
28
|
+
/^```([\w-]+)?[\t ]+file="([^"]+)"[^\n]*\n(?:([\s\S]*?)\n)?```$/gm;
|
|
29
|
+
const IGNORE_LINE_RE = /^.*\/\/\s*ngmd-ignore-line\s*$/;
|
|
30
|
+
|
|
31
|
+
function loadFile(spec: string): { code: string; rangeFragment: string } {
|
|
32
|
+
const [path, range] = spec.split('#');
|
|
33
|
+
const full = resolve(process.cwd(), path);
|
|
34
|
+
let content = readFileSync(full, 'utf8');
|
|
35
|
+
|
|
36
|
+
let rangeFragment = '';
|
|
37
|
+
if (range) {
|
|
38
|
+
const m = range.match(/^L(\d+)(?:-L?(\d+))?$/);
|
|
39
|
+
if (m) {
|
|
40
|
+
const start = parseInt(m[1], 10);
|
|
41
|
+
const end = m[2] ? parseInt(m[2], 10) : start;
|
|
42
|
+
const lines = content.split('\n');
|
|
43
|
+
content = lines.slice(start - 1, end).join('\n');
|
|
44
|
+
rangeFragment = m[2] ? `#L${start}-L${end}` : `#L${start}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const filtered = content
|
|
49
|
+
.split('\n')
|
|
50
|
+
.filter((l) => !IGNORE_LINE_RE.test(l))
|
|
51
|
+
.join('\n');
|
|
52
|
+
|
|
53
|
+
return { code: filtered.replace(/\n+$/, ''), rangeFragment };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function githubBlobUrl(filePath: string, rangeFragment: string): string {
|
|
57
|
+
const repo = config.site.githubUrl.replace(/\.git$/, '');
|
|
58
|
+
return `${repo}/blob/main/${filePath}${rangeFragment}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const ngmdCodeImportExtension: MarkedExtension = {
|
|
62
|
+
hooks: {
|
|
63
|
+
async preprocess(markdown: string): Promise<string> {
|
|
64
|
+
if (!/^```[\w-]*[\t ]+file="/m.test(markdown)) return markdown;
|
|
65
|
+
|
|
66
|
+
const matches: {
|
|
67
|
+
start: number;
|
|
68
|
+
end: number;
|
|
69
|
+
lang: string;
|
|
70
|
+
filePath: string;
|
|
71
|
+
rangeFragment: string;
|
|
72
|
+
code: string;
|
|
73
|
+
}[] = [];
|
|
74
|
+
|
|
75
|
+
const re = new RegExp(FENCE_RE.source, FENCE_RE.flags);
|
|
76
|
+
let m: RegExpExecArray | null;
|
|
77
|
+
while ((m = re.exec(markdown)) !== null) {
|
|
78
|
+
const lang = m[1] ?? '';
|
|
79
|
+
const spec = m[2];
|
|
80
|
+
try {
|
|
81
|
+
const { code, rangeFragment } = loadFile(spec);
|
|
82
|
+
matches.push({
|
|
83
|
+
start: m.index,
|
|
84
|
+
end: m.index + m[0].length,
|
|
85
|
+
lang,
|
|
86
|
+
filePath: spec.split('#')[0],
|
|
87
|
+
rangeFragment,
|
|
88
|
+
code,
|
|
89
|
+
});
|
|
90
|
+
} catch (e) {
|
|
91
|
+
const msg = (e as Error).message ?? String(e);
|
|
92
|
+
console.warn(`[ngmd-code-import] failed to load "${spec}": ${msg}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (matches.length === 0) return markdown;
|
|
96
|
+
|
|
97
|
+
const highlighter = await getHighlighter();
|
|
98
|
+
const renders = matches.map((mt) => {
|
|
99
|
+
const safeLang = LANGS.includes(mt.lang) ? mt.lang : 'text';
|
|
100
|
+
const codeHtml = highlighter.codeToHtml(mt.code, {
|
|
101
|
+
lang: safeLang,
|
|
102
|
+
theme: 'github-dark',
|
|
103
|
+
});
|
|
104
|
+
const headerLabel = mt.filePath + (mt.rangeFragment || '');
|
|
105
|
+
const headerHtml = `<a class="ngmd-code-import__header" href="${escapeHtml(githubBlobUrl(mt.filePath, mt.rangeFragment))}" target="_blank" rel="noopener noreferrer">${escapeHtml(headerLabel)}</a>`;
|
|
106
|
+
return `<div class="ngmd-code-import">${headerHtml}${codeHtml}</div>`;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let result = markdown;
|
|
110
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
111
|
+
const mt = matches[i];
|
|
112
|
+
result =
|
|
113
|
+
result.slice(0, mt.start) + `\n\n${renders[i]}\n\n` + result.slice(mt.end);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Tokens } from 'marked';
|
|
2
|
+
|
|
3
|
+
interface NgmdImageToken extends Tokens.Generic {
|
|
4
|
+
type: 'ngmd-image';
|
|
5
|
+
src: string;
|
|
6
|
+
alt: string;
|
|
7
|
+
caption?: string;
|
|
8
|
+
width?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const tagRule = /^<ngmd-image([^>]*)\/>/s;
|
|
12
|
+
const attrRule = (name: string) => new RegExp(`${name}="([^"]*)"`);
|
|
13
|
+
|
|
14
|
+
export const ngmdImageExtension = {
|
|
15
|
+
name: 'ngmd-image',
|
|
16
|
+
level: 'block' as const,
|
|
17
|
+
start(src: string) {
|
|
18
|
+
return src.match(/^\s*<ngmd-image/m)?.index;
|
|
19
|
+
},
|
|
20
|
+
tokenizer(src: string): NgmdImageToken | undefined {
|
|
21
|
+
const match = tagRule.exec(src);
|
|
22
|
+
if (!match) return undefined;
|
|
23
|
+
const attrs = match[1].trim();
|
|
24
|
+
const srcMatch = attrRule('src').exec(attrs);
|
|
25
|
+
if (!srcMatch) return undefined;
|
|
26
|
+
return {
|
|
27
|
+
type: 'ngmd-image',
|
|
28
|
+
raw: match[0],
|
|
29
|
+
src: srcMatch[1],
|
|
30
|
+
alt: attrRule('alt').exec(attrs)?.[1] ?? '',
|
|
31
|
+
caption: attrRule('caption').exec(attrs)?.[1],
|
|
32
|
+
width: attrRule('width').exec(attrs)?.[1],
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
renderer(token: NgmdImageToken) {
|
|
36
|
+
const widthAttr = token.width ? token.width.replace(/"/g, '') : '';
|
|
37
|
+
const alt = token.alt.replace(/"/g, '"');
|
|
38
|
+
const caption = token.caption ? token.caption.replace(/"/g, '"') : '';
|
|
39
|
+
return `<div class="ngmd-image" data-image-src="${token.src}" data-image-alt="${alt}" data-image-caption="${caption}" data-image-width="${widthAttr}"></div>`;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { MarkedExtension, Tokens } from 'marked';
|
|
2
|
+
import config from '../ngmd.config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Inline keyword auto-linking. Any `*Keyword` token (where `Keyword` is
|
|
6
|
+
* defined in `ngmd.config.ts > keywords`) becomes a link.
|
|
7
|
+
*
|
|
8
|
+
* *AnalogJS → <a href="https://analogjs.org">AnalogJS</a>
|
|
9
|
+
* *NgMd → <a href="/welcome">NgMd</a>
|
|
10
|
+
*
|
|
11
|
+
* Unknown keywords log a one-line warning and fall through to the default
|
|
12
|
+
* inline tokenizer — they render as literal `*Keyword` text. External URLs
|
|
13
|
+
* get `target="_blank" rel="noopener noreferrer"` automatically.
|
|
14
|
+
*
|
|
15
|
+
* Lives in the inline tokenizer chain, so it never fires inside fenced code
|
|
16
|
+
* blocks or inline code (those are block-level and tokenized first).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface NgmdKeywordToken extends Tokens.Generic {
|
|
20
|
+
type: 'ngmdKeyword';
|
|
21
|
+
keyword: string;
|
|
22
|
+
url: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const KEYWORD_RE = /^\*([A-Z][a-zA-Z0-9]+)\b/;
|
|
26
|
+
const HINT_RE = /\*[A-Z]/;
|
|
27
|
+
const warned = new Set<string>();
|
|
28
|
+
|
|
29
|
+
function lookup(keyword: string): string | undefined {
|
|
30
|
+
return config.keywords?.[keyword];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function escapeAttr(s: string): string {
|
|
34
|
+
return s.replace(/"/g, '"');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const ngmdKeywordsExtension: MarkedExtension = {
|
|
38
|
+
extensions: [
|
|
39
|
+
{
|
|
40
|
+
name: 'ngmdKeyword',
|
|
41
|
+
level: 'inline',
|
|
42
|
+
start(src: string) {
|
|
43
|
+
return src.match(HINT_RE)?.index;
|
|
44
|
+
},
|
|
45
|
+
tokenizer(src: string): NgmdKeywordToken | undefined {
|
|
46
|
+
const m = KEYWORD_RE.exec(src);
|
|
47
|
+
if (!m) return undefined;
|
|
48
|
+
const url = lookup(m[1]);
|
|
49
|
+
if (!url) {
|
|
50
|
+
if (!warned.has(m[1])) {
|
|
51
|
+
warned.add(m[1]);
|
|
52
|
+
console.warn(
|
|
53
|
+
`[ngmd-keywords] unknown keyword "${m[1]}" — add it to ngmd.config.ts > keywords or escape the asterisk.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
type: 'ngmdKeyword',
|
|
60
|
+
raw: m[0],
|
|
61
|
+
keyword: m[1],
|
|
62
|
+
url,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
renderer(token: Tokens.Generic) {
|
|
66
|
+
const t = token as NgmdKeywordToken;
|
|
67
|
+
const isExternal = /^https?:\/\//.test(t.url);
|
|
68
|
+
const targetAttrs = isExternal
|
|
69
|
+
? ' target="_blank" rel="noopener noreferrer"'
|
|
70
|
+
: '';
|
|
71
|
+
return `<a href="${escapeAttr(t.url)}"${targetAttrs}>${t.keyword}</a>`;
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Tokens } from 'marked';
|
|
2
|
+
|
|
3
|
+
interface NgmdVideoToken extends Tokens.Generic {
|
|
4
|
+
type: 'ngmd-video';
|
|
5
|
+
src: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const tagRule = /^<ngmd-video([^>]*)\/>/s;
|
|
10
|
+
const srcRule = /src="([^"]*)"/;
|
|
11
|
+
const titleRule = /title="([^"]*)"/;
|
|
12
|
+
|
|
13
|
+
function buildEmbedUrl(src: string): string {
|
|
14
|
+
if (src.startsWith('https://www.youtube.com/embed/')) return src;
|
|
15
|
+
const yt = src.match(/youtube\.com\/watch\?v=([\w-]+)/);
|
|
16
|
+
if (yt) return `https://www.youtube.com/embed/${yt[1]}`;
|
|
17
|
+
const ytShort = src.match(/youtu\.be\/([\w-]+)/);
|
|
18
|
+
if (ytShort) return `https://www.youtube.com/embed/${ytShort[1]}`;
|
|
19
|
+
const vm = src.match(/vimeo\.com\/(\d+)/);
|
|
20
|
+
if (vm) return `https://player.vimeo.com/video/${vm[1]}`;
|
|
21
|
+
return src;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ngmdVideoExtension = {
|
|
25
|
+
name: 'ngmd-video',
|
|
26
|
+
level: 'block' as const,
|
|
27
|
+
start(src: string) {
|
|
28
|
+
return src.match(/^\s*<ngmd-video/m)?.index;
|
|
29
|
+
},
|
|
30
|
+
tokenizer(src: string): NgmdVideoToken | undefined {
|
|
31
|
+
const match = tagRule.exec(src);
|
|
32
|
+
if (!match) return undefined;
|
|
33
|
+
const attrs = match[1].trim();
|
|
34
|
+
const srcMatch = srcRule.exec(attrs);
|
|
35
|
+
if (!srcMatch) return undefined;
|
|
36
|
+
return {
|
|
37
|
+
type: 'ngmd-video',
|
|
38
|
+
raw: match[0],
|
|
39
|
+
src: srcMatch[1],
|
|
40
|
+
title: titleRule.exec(attrs)?.[1],
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
renderer(token: NgmdVideoToken) {
|
|
44
|
+
const url = buildEmbedUrl(token.src);
|
|
45
|
+
const title = (token.title ?? 'Video player').replace(/"/g, '"');
|
|
46
|
+
return `<div class="ngmd-video" data-video-src="${url}" data-video-title="${title}"></div>`;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createHighlighter, type Highlighter } from 'shiki';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared shiki highlighter instance used by every build-time fence extension
|
|
5
|
+
* that pre-renders code (code-group, code-import, code-highlight). Loading
|
|
6
|
+
* the highlighter is expensive (parses tmGrammar files for every language),
|
|
7
|
+
* so we keep a single promise per process.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let highlighterPromise: Promise<Highlighter> | null = null;
|
|
11
|
+
|
|
12
|
+
export const LANGS = [
|
|
13
|
+
'bash',
|
|
14
|
+
'json',
|
|
15
|
+
'ts',
|
|
16
|
+
'tsx',
|
|
17
|
+
'js',
|
|
18
|
+
'jsx',
|
|
19
|
+
'html',
|
|
20
|
+
'css',
|
|
21
|
+
'md',
|
|
22
|
+
'angular-html',
|
|
23
|
+
'angular-ts',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function getHighlighter(): Promise<Highlighter> {
|
|
27
|
+
if (!highlighterPromise) {
|
|
28
|
+
highlighterPromise = createHighlighter({
|
|
29
|
+
themes: ['github-dark'],
|
|
30
|
+
langs: LANGS,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return highlighterPromise;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function escapeHtml(s: string): string {
|
|
37
|
+
return s
|
|
38
|
+
.replace(/&/g, '&')
|
|
39
|
+
.replace(/</g, '<')
|
|
40
|
+
.replace(/>/g, '>')
|
|
41
|
+
.replace(/"/g, '"');
|
|
42
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NgMd site configuration.
|
|
3
|
+
*
|
|
4
|
+
* Edit this file to customise navigation, site metadata, and external links.
|
|
5
|
+
* Sidebar, command palette, breadcrumb, and header all read from here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface NavItem {
|
|
9
|
+
label: string;
|
|
10
|
+
href: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface NavSection {
|
|
14
|
+
label: string;
|
|
15
|
+
items: NavItem[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SiteConfig {
|
|
19
|
+
/** Brand name shown in the header next to the logo. */
|
|
20
|
+
name: string;
|
|
21
|
+
/** One-liner description used in meta tags + social previews. */
|
|
22
|
+
description: string;
|
|
23
|
+
/** Short tagline shown after the brand in the homepage `<title>`. */
|
|
24
|
+
tagline?: string;
|
|
25
|
+
/** Public origin (no trailing slash). Used by sitemap.xml + robots.txt. */
|
|
26
|
+
url: string;
|
|
27
|
+
/** Repository URL. Powers the GitHub icon in the header. */
|
|
28
|
+
githubUrl: string;
|
|
29
|
+
/** Optional social / community links rendered in the header. */
|
|
30
|
+
links?: {
|
|
31
|
+
twitter?: string;
|
|
32
|
+
discord?: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NgmdConfig {
|
|
37
|
+
site: SiteConfig;
|
|
38
|
+
/** Sidebar sections, in render order. */
|
|
39
|
+
nav: NavSection[];
|
|
40
|
+
/**
|
|
41
|
+
* Inline-link keywords. In any `.md` body, `*Keyword` resolves to a link
|
|
42
|
+
* pointing at the configured URL. Unknown keywords log a warning and fall
|
|
43
|
+
* back to literal `*Keyword` text. Change the URL here once, every doc
|
|
44
|
+
* follows.
|
|
45
|
+
*/
|
|
46
|
+
keywords?: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const config: NgmdConfig = {
|
|
50
|
+
site: {
|
|
51
|
+
name: 'NgMd',
|
|
52
|
+
description:
|
|
53
|
+
'Modern Angular docs-site starter built on AnalogJS, Spartan UI, and Tailwind.',
|
|
54
|
+
tagline: 'Angular docs starter',
|
|
55
|
+
url: 'https://ngmd.netlify.app',
|
|
56
|
+
githubUrl: 'https://github.com/erkamyaman/ngmd',
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
keywords: {
|
|
60
|
+
NgMd: '/welcome',
|
|
61
|
+
AnalogJS: 'https://analogjs.org',
|
|
62
|
+
Angular: 'https://angular.dev',
|
|
63
|
+
Tailwind: 'https://tailwindcss.com',
|
|
64
|
+
Shiki: 'https://shiki.style',
|
|
65
|
+
Marked: 'https://marked.js.org',
|
|
66
|
+
Vite: 'https://vitejs.dev',
|
|
67
|
+
Spartan: 'https://www.spartan.ng',
|
|
68
|
+
VitePress: 'https://vitepress.dev',
|
|
69
|
+
Starlight: 'https://starlight.astro.build',
|
|
70
|
+
Docusaurus: 'https://docusaurus.io',
|
|
71
|
+
Nextra: 'https://nextra.site',
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
nav: [
|
|
75
|
+
{
|
|
76
|
+
label: 'Getting Started',
|
|
77
|
+
items: [{ label: 'Welcome', href: '/welcome' }],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default config;
|
|
83
|
+
|
|
84
|
+
/** Flattened list of all nav items, useful for command palette / search. */
|
|
85
|
+
export const navItems = config.nav.flatMap((section) =>
|
|
86
|
+
section.items.map((item) => ({
|
|
87
|
+
label: item.label,
|
|
88
|
+
href: item.href,
|
|
89
|
+
section: section.label,
|
|
90
|
+
})),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
/** Map of last URL segment to its human label, useful for breadcrumb. */
|
|
94
|
+
export const navLabels = Object.fromEntries(
|
|
95
|
+
config.nav.flatMap((section) =>
|
|
96
|
+
section.items.map((item) => [item.href.split('/').pop() ?? '', item.label]),
|
|
97
|
+
),
|
|
98
|
+
);
|