create-eziwiki 0.1.1 → 0.2.0
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 +3 -2
- package/package.json +1 -1
- package/template/app/[...slug]/page.tsx +3 -1
- package/template/app/layout.tsx +3 -1
- package/template/components/graph/GraphView.tsx +20 -7
- package/template/components/layout/LocalGraph.tsx +52 -0
- package/template/components/layout/MobileMenu.tsx +16 -1
- package/template/components/layout/PageLayout.tsx +11 -3
- package/template/components/layout/Sidebar.tsx +30 -2
- package/template/components/markdown/LinkPreview.tsx +164 -0
- package/template/components/markdown/MarkdownContent.tsx +3 -1
- package/template/lib/content/assets.ts +158 -0
- package/template/lib/content/excerpt.test.ts +68 -0
- package/template/lib/content/excerpt.ts +142 -0
- package/template/lib/graph/build.ts +55 -0
- package/template/lib/markdown/rehype-plugins.ts +5 -0
- package/template/lib/markdown/remark-wikilink.ts +201 -14
- package/template/lib/markdown/render.ts +110 -8
- package/template/lib/markdown/wikilink.test.ts +30 -0
- package/template/lib/markdown/wikilink.ts +12 -4
- package/template/lib/payload/schema.ts +1 -0
- package/template/lib/payload/types.ts +7 -0
- package/template/package-lock.json +3 -83
- package/template/package.json +2 -0
- package/template/styles/markdown.css +74 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { unified, type Processor } from 'unified';
|
|
2
|
+
import { VFile } from 'vfile';
|
|
3
|
+
import type { Root, RootContent } from 'mdast';
|
|
2
4
|
import remarkParse from 'remark-parse';
|
|
3
5
|
import remarkGfm from 'remark-gfm';
|
|
4
6
|
import remarkMath from 'remark-math';
|
|
@@ -16,13 +18,17 @@ import {
|
|
|
16
18
|
rehypeInternalLinks,
|
|
17
19
|
type Heading,
|
|
18
20
|
} from './rehype-plugins';
|
|
19
|
-
import { remarkWikiLinks, type WikiLinkTarget } from './remark-wikilink';
|
|
21
|
+
import { remarkWikiLinks, type WikiLinkTarget, type TranscludeTarget } from './remark-wikilink';
|
|
20
22
|
import { getUsedLanguages } from './languages';
|
|
21
23
|
import { cached } from '../cache';
|
|
22
24
|
import { BASE_PATH } from '../basePath';
|
|
23
25
|
import { getUrlMap } from '../navigation/urlMap';
|
|
24
26
|
import { getDoc } from '../content/registry';
|
|
25
27
|
import { resolveTarget } from '../content/resolver';
|
|
28
|
+
import { resolveAsset } from '../content/assets';
|
|
29
|
+
import { getExcerpt } from '../content/excerpt';
|
|
30
|
+
import GithubSlugger from 'github-slugger';
|
|
31
|
+
import { toString as mdastToString } from 'mdast-util-to-string';
|
|
26
32
|
import { docPathToUrl } from '../navigation/url';
|
|
27
33
|
|
|
28
34
|
/**
|
|
@@ -77,7 +83,88 @@ function resolveWikiLink(target: string): WikiLinkTarget | null {
|
|
|
77
83
|
if (!doc) return null;
|
|
78
84
|
|
|
79
85
|
const url = docPathToUrl(getUrlMap(), doc.path);
|
|
80
|
-
|
|
86
|
+
if (!url) return null;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
url: `/${url}/`,
|
|
90
|
+
title: doc.title,
|
|
91
|
+
excerpt: getExcerpt(doc.path),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolves an embed target to a file under `public/`.
|
|
97
|
+
*
|
|
98
|
+
* @param target - Raw target text from inside the brackets
|
|
99
|
+
* @returns The file's URL, or null when nothing matches
|
|
100
|
+
*/
|
|
101
|
+
function resolveWikiEmbed(target: string): { url: string } | null {
|
|
102
|
+
const asset = resolveAsset(target);
|
|
103
|
+
return asset ? { url: asset.url } : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Parser for included documents; no rendering plugins, so it stays cheap. */
|
|
107
|
+
const transclusionParser = unified().use(remarkParse).use(remarkGfm).use(remarkMath);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Narrows a document's nodes to the section a heading names.
|
|
111
|
+
*
|
|
112
|
+
* The section runs from the matching heading to the next one at the same level
|
|
113
|
+
* or above, which is what a reader means by "that part of the page". The
|
|
114
|
+
* heading itself is kept: dropping it would leave the included text with no
|
|
115
|
+
* indication of what it is.
|
|
116
|
+
*
|
|
117
|
+
* @param nodes - The whole document's nodes
|
|
118
|
+
* @param anchor - Heading slug from after the `#`
|
|
119
|
+
* @returns The section's nodes, or null when no heading matches
|
|
120
|
+
*/
|
|
121
|
+
function sliceSection(nodes: RootContent[], anchor: string): RootContent[] | null {
|
|
122
|
+
const slugger = new GithubSlugger();
|
|
123
|
+
const wanted = anchor.toLowerCase();
|
|
124
|
+
|
|
125
|
+
let start = -1;
|
|
126
|
+
let depth = 0;
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
129
|
+
const node = nodes[i];
|
|
130
|
+
if (node.type !== 'heading') continue;
|
|
131
|
+
|
|
132
|
+
// Slugs are assigned in document order, and repeats are numbered, so every
|
|
133
|
+
// heading has to pass through the slugger even before the match is found.
|
|
134
|
+
const slug = slugger.slug(mdastToString(node));
|
|
135
|
+
|
|
136
|
+
if (start === -1) {
|
|
137
|
+
if (slug !== wanted) continue;
|
|
138
|
+
start = i;
|
|
139
|
+
depth = node.depth;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (node.depth <= depth) return nodes.slice(start, i);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return start === -1 ? null : nodes.slice(start);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolves an embed target to the document content it names.
|
|
151
|
+
*
|
|
152
|
+
* @param target - Raw target text from inside the brackets
|
|
153
|
+
* @param anchor - Heading slug, when the embed named a section
|
|
154
|
+
* @returns The document's nodes and identity, or null when it does not resolve
|
|
155
|
+
*/
|
|
156
|
+
function resolveWikiTransclusion(target: string, anchor?: string): TranscludeTarget | null {
|
|
157
|
+
const { doc } = resolveTarget(target);
|
|
158
|
+
if (!doc) return null;
|
|
159
|
+
|
|
160
|
+
const url = docPathToUrl(getUrlMap(), doc.path);
|
|
161
|
+
if (!url) return null;
|
|
162
|
+
|
|
163
|
+
const tree = transclusionParser.parse(doc.content) as Root;
|
|
164
|
+
const nodes = anchor ? sliceSection(tree.children, anchor) : tree.children;
|
|
165
|
+
if (!nodes || nodes.length === 0) return null;
|
|
166
|
+
|
|
167
|
+
return { path: doc.path, url: `/${url}/`, title: doc.title, nodes };
|
|
81
168
|
}
|
|
82
169
|
|
|
83
170
|
/**
|
|
@@ -99,7 +186,11 @@ function createProcessor(): Processor {
|
|
|
99
186
|
.use(remarkParse)
|
|
100
187
|
.use(remarkGfm)
|
|
101
188
|
.use(remarkMath)
|
|
102
|
-
.use(remarkWikiLinks,
|
|
189
|
+
.use(remarkWikiLinks, {
|
|
190
|
+
link: resolveWikiLink,
|
|
191
|
+
embed: resolveWikiEmbed,
|
|
192
|
+
transclude: resolveWikiTransclusion,
|
|
193
|
+
})
|
|
103
194
|
.use(remarkRehype, { allowDangerousHtml: true })
|
|
104
195
|
.use(rehypeRaw)
|
|
105
196
|
.use(rehypeSlug)
|
|
@@ -150,6 +241,9 @@ function getProcessor(): Processor {
|
|
|
150
241
|
* Compiles a Markdown string to HTML and extracts its headings.
|
|
151
242
|
*
|
|
152
243
|
* @param markdown - Markdown source, with frontmatter already stripped
|
|
244
|
+
* @param docPath - Path of the document being rendered, when it has one; a
|
|
245
|
+
* transclusion naming it is refused rather than nesting a copy of the page
|
|
246
|
+
* inside itself
|
|
153
247
|
* @returns The rendered HTML and the headings found in it
|
|
154
248
|
*
|
|
155
249
|
* @example
|
|
@@ -158,12 +252,20 @@ function getProcessor(): Processor {
|
|
|
158
252
|
* headings; // [{ id: 'setup', text: 'Setup', depth: 2 }]
|
|
159
253
|
* ```
|
|
160
254
|
*/
|
|
161
|
-
export async function renderMarkdown(
|
|
162
|
-
|
|
255
|
+
export async function renderMarkdown(
|
|
256
|
+
markdown: string,
|
|
257
|
+
docPath?: string,
|
|
258
|
+
): Promise<RenderedMarkdown> {
|
|
259
|
+
const file = new VFile(markdown);
|
|
260
|
+
// Seeds the transclusion stack, so a document that includes itself is caught
|
|
261
|
+
// at the first step rather than after rendering one copy of itself.
|
|
262
|
+
if (docPath) file.data.docPath = docPath;
|
|
263
|
+
|
|
264
|
+
const processed = await getProcessor().process(file);
|
|
163
265
|
|
|
164
266
|
return {
|
|
165
|
-
html: String(
|
|
166
|
-
headings: (
|
|
267
|
+
html: String(processed),
|
|
268
|
+
headings: (processed.data.headings as Heading[] | undefined) ?? [],
|
|
167
269
|
};
|
|
168
270
|
}
|
|
169
271
|
|
|
@@ -186,7 +288,7 @@ export async function renderDoc(docPath: string): Promise<RenderedMarkdown | nul
|
|
|
186
288
|
const doc = getDoc(docPath);
|
|
187
289
|
if (!doc) return null;
|
|
188
290
|
|
|
189
|
-
const rendered = await renderMarkdown(doc.content);
|
|
291
|
+
const rendered = await renderMarkdown(doc.content, doc.path);
|
|
190
292
|
cache.set(docPath, rendered);
|
|
191
293
|
|
|
192
294
|
return rendered;
|
|
@@ -89,3 +89,33 @@ describe('findWikiLinks', () => {
|
|
|
89
89
|
expect(findWikiLinks('x [[a|B]] y')[0].raw).toBe('[[a|B]]');
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
describe('embeds', () => {
|
|
94
|
+
it('marks a leading ! as an embed', () => {
|
|
95
|
+
const [link] = findWikiLinks('![[diagram.png]]');
|
|
96
|
+
|
|
97
|
+
expect(link.embed).toBe(true);
|
|
98
|
+
expect(link.target).toBe('diagram.png');
|
|
99
|
+
expect(link.raw).toBe('![[diagram.png]]');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('leaves a plain link unmarked', () => {
|
|
103
|
+
const [link] = findWikiLinks('[[diagram.png]]');
|
|
104
|
+
|
|
105
|
+
expect(link.embed).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// The `!` has to be part of the match. Matching only the brackets would
|
|
109
|
+
// leave it behind as literal text in front of the rendered node.
|
|
110
|
+
it('consumes the ! rather than leaving it in the text', () => {
|
|
111
|
+
const [link] = findWikiLinks('before ![[a]] after');
|
|
112
|
+
|
|
113
|
+
expect(link.raw.startsWith('!')).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('accepts a label on an embed', () => {
|
|
117
|
+
const [link] = findWikiLinks('![[diagram.png|Architecture]]');
|
|
118
|
+
|
|
119
|
+
expect(link).toMatchObject({ target: 'diagram.png', label: 'Architecture', embed: true });
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
/**
|
|
9
9
|
* Matches a wiki link and captures its contents.
|
|
10
10
|
*
|
|
11
|
+
* The leading `!` is part of the match so that `![[diagram.png]]` is recognised
|
|
12
|
+
* as an embed rather than leaving a stray `!` in the text ahead of a link.
|
|
13
|
+
*
|
|
11
14
|
* Deliberately refuses `]` and newlines inside the brackets: an unterminated
|
|
12
15
|
* `[[` should stay literal text rather than swallowing the rest of a paragraph.
|
|
13
16
|
*/
|
|
14
|
-
export const WIKILINK_PATTERN =
|
|
17
|
+
export const WIKILINK_PATTERN = /(!?)\[\[([^\]\n]+)\]\]/g;
|
|
15
18
|
|
|
16
19
|
/** The parts of a wiki link. */
|
|
17
20
|
export interface WikiLink {
|
|
@@ -21,6 +24,8 @@ export interface WikiLink {
|
|
|
21
24
|
anchor?: string;
|
|
22
25
|
/** Display text, when the author supplied one after '|' */
|
|
23
26
|
label?: string;
|
|
27
|
+
/** Written as `![[…]]`, asking for the target to be shown rather than linked */
|
|
28
|
+
embed: boolean;
|
|
24
29
|
/** The full matched source, e.g. '[[guide#setup|Setup]]' */
|
|
25
30
|
raw: string;
|
|
26
31
|
}
|
|
@@ -33,21 +38,23 @@ export interface WikiLink {
|
|
|
33
38
|
* - `[[target|label]]`
|
|
34
39
|
* - `[[target#anchor]]`
|
|
35
40
|
* - `[[target#anchor|label]]`
|
|
41
|
+
* - any of the above prefixed with `!`, to embed rather than link
|
|
36
42
|
*
|
|
37
43
|
* The label is split off first, so a `|` inside it is preserved and a `#` in
|
|
38
44
|
* the label is not mistaken for an anchor.
|
|
39
45
|
*
|
|
40
46
|
* @param inner - Text between the brackets
|
|
41
47
|
* @param raw - The full matched source, stored on the result
|
|
48
|
+
* @param embed - Whether the source carried a leading `!`
|
|
42
49
|
* @returns The parsed link, or null when the target is empty
|
|
43
50
|
*
|
|
44
51
|
* @example
|
|
45
52
|
* ```typescript
|
|
46
53
|
* parseWikiLink('guides/setup#step-1|Step one', '[[guides/setup#step-1|Step one]]');
|
|
47
|
-
* // { target: 'guides/setup', anchor: 'step-1', label: 'Step one', raw: '...' }
|
|
54
|
+
* // { target: 'guides/setup', anchor: 'step-1', label: 'Step one', embed: false, raw: '...' }
|
|
48
55
|
* ```
|
|
49
56
|
*/
|
|
50
|
-
export function parseWikiLink(inner: string, raw: string): WikiLink | null {
|
|
57
|
+
export function parseWikiLink(inner: string, raw: string, embed = false): WikiLink | null {
|
|
51
58
|
const pipe = inner.indexOf('|');
|
|
52
59
|
const label = pipe === -1 ? undefined : inner.slice(pipe + 1).trim();
|
|
53
60
|
const locator = (pipe === -1 ? inner : inner.slice(0, pipe)).trim();
|
|
@@ -63,6 +70,7 @@ export function parseWikiLink(inner: string, raw: string): WikiLink | null {
|
|
|
63
70
|
target,
|
|
64
71
|
anchor,
|
|
65
72
|
label: label || undefined,
|
|
73
|
+
embed,
|
|
66
74
|
raw,
|
|
67
75
|
};
|
|
68
76
|
}
|
|
@@ -77,7 +85,7 @@ export function findWikiLinks(text: string): WikiLink[] {
|
|
|
77
85
|
const links: WikiLink[] = [];
|
|
78
86
|
|
|
79
87
|
for (const match of text.matchAll(WIKILINK_PATTERN)) {
|
|
80
|
-
const parsed = parseWikiLink(match[
|
|
88
|
+
const parsed = parseWikiLink(match[2], match[0], match[1] === '!');
|
|
81
89
|
if (parsed) links.push(parsed);
|
|
82
90
|
}
|
|
83
91
|
|
|
@@ -14,6 +14,7 @@ export const payloadSchema = {
|
|
|
14
14
|
description: { type: 'string', minLength: 1 },
|
|
15
15
|
favicon: { type: 'string' },
|
|
16
16
|
baseUrl: { type: 'string', format: 'uri' },
|
|
17
|
+
repoUrl: { type: 'string', format: 'uri' },
|
|
17
18
|
urlStrategy: { type: 'string', enum: ['path', 'hash'] },
|
|
18
19
|
autoNavigation: { type: 'boolean' },
|
|
19
20
|
seo: {
|
|
@@ -30,6 +30,13 @@ export interface GlobalConfig {
|
|
|
30
30
|
favicon?: string;
|
|
31
31
|
/** Base URL for the site */
|
|
32
32
|
baseUrl?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Source repository for this site.
|
|
35
|
+
*
|
|
36
|
+
* Shown as a link in the sidebar when set, and omitted entirely when not, so
|
|
37
|
+
* a private or unpublished wiki does not advertise one.
|
|
38
|
+
*/
|
|
39
|
+
repoUrl?: string;
|
|
33
40
|
/**
|
|
34
41
|
* How content paths are expressed in URLs.
|
|
35
42
|
*
|
|
@@ -7,14 +7,17 @@
|
|
|
7
7
|
"": {
|
|
8
8
|
"name": "my-wiki",
|
|
9
9
|
"version": "0.1.0",
|
|
10
|
+
"license": "MIT",
|
|
10
11
|
"dependencies": {
|
|
11
12
|
"@shikijs/rehype": "^4.3.1",
|
|
12
13
|
"ajv": "^8.12.0",
|
|
13
14
|
"ajv-formats": "^3.0.1",
|
|
15
|
+
"github-slugger": "^2.0.0",
|
|
14
16
|
"gray-matter": "^4.0.3",
|
|
15
17
|
"hast-util-to-string": "^3.0.1",
|
|
16
18
|
"katex": "^0.16.47",
|
|
17
19
|
"lucide-react": "^0.554.0",
|
|
20
|
+
"mdast-util-to-string": "^4.0.0",
|
|
18
21
|
"minisearch": "^7.2.0",
|
|
19
22
|
"next": "^14.2.0",
|
|
20
23
|
"react": "^18.3.0",
|
|
@@ -1047,15 +1050,6 @@
|
|
|
1047
1050
|
"url": "https://opencollective.com/pkgr"
|
|
1048
1051
|
}
|
|
1049
1052
|
},
|
|
1050
|
-
"node_modules/@polka/url": {
|
|
1051
|
-
"version": "1.0.0-next.29",
|
|
1052
|
-
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
|
1053
|
-
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
|
1054
|
-
"dev": true,
|
|
1055
|
-
"license": "MIT",
|
|
1056
|
-
"optional": true,
|
|
1057
|
-
"peer": true
|
|
1058
|
-
},
|
|
1059
1053
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
|
1060
1054
|
"version": "4.53.2",
|
|
1061
1055
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz",
|
|
@@ -2222,30 +2216,6 @@
|
|
|
2222
2216
|
"url": "https://opencollective.com/vitest"
|
|
2223
2217
|
}
|
|
2224
2218
|
},
|
|
2225
|
-
"node_modules/@vitest/ui": {
|
|
2226
|
-
"version": "4.0.9",
|
|
2227
|
-
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.9.tgz",
|
|
2228
|
-
"integrity": "sha512-6HV2HHl9aRJ09TlYj/WAQxaa797Ezb5u0LpgabthlASAUAWKgw/W1DSPX7t848mMZmIUvzZgnUHGIylAoYHP0w==",
|
|
2229
|
-
"dev": true,
|
|
2230
|
-
"license": "MIT",
|
|
2231
|
-
"optional": true,
|
|
2232
|
-
"peer": true,
|
|
2233
|
-
"dependencies": {
|
|
2234
|
-
"@vitest/utils": "4.0.9",
|
|
2235
|
-
"fflate": "^0.8.2",
|
|
2236
|
-
"flatted": "^3.3.3",
|
|
2237
|
-
"pathe": "^2.0.3",
|
|
2238
|
-
"sirv": "^3.0.2",
|
|
2239
|
-
"tinyglobby": "^0.2.15",
|
|
2240
|
-
"tinyrainbow": "^3.0.3"
|
|
2241
|
-
},
|
|
2242
|
-
"funding": {
|
|
2243
|
-
"url": "https://opencollective.com/vitest"
|
|
2244
|
-
},
|
|
2245
|
-
"peerDependencies": {
|
|
2246
|
-
"vitest": "4.0.9"
|
|
2247
|
-
}
|
|
2248
|
-
},
|
|
2249
2219
|
"node_modules/@vitest/utils": {
|
|
2250
2220
|
"version": "4.0.9",
|
|
2251
2221
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.9.tgz",
|
|
@@ -4288,15 +4258,6 @@
|
|
|
4288
4258
|
"reusify": "^1.0.4"
|
|
4289
4259
|
}
|
|
4290
4260
|
},
|
|
4291
|
-
"node_modules/fflate": {
|
|
4292
|
-
"version": "0.8.2",
|
|
4293
|
-
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
|
4294
|
-
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
|
4295
|
-
"dev": true,
|
|
4296
|
-
"license": "MIT",
|
|
4297
|
-
"optional": true,
|
|
4298
|
-
"peer": true
|
|
4299
|
-
},
|
|
4300
4261
|
"node_modules/file-entry-cache": {
|
|
4301
4262
|
"version": "6.0.1",
|
|
4302
4263
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
|
@@ -6793,18 +6754,6 @@
|
|
|
6793
6754
|
"integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==",
|
|
6794
6755
|
"license": "MIT"
|
|
6795
6756
|
},
|
|
6796
|
-
"node_modules/mrmime": {
|
|
6797
|
-
"version": "2.0.1",
|
|
6798
|
-
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
|
6799
|
-
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
|
6800
|
-
"dev": true,
|
|
6801
|
-
"license": "MIT",
|
|
6802
|
-
"optional": true,
|
|
6803
|
-
"peer": true,
|
|
6804
|
-
"engines": {
|
|
6805
|
-
"node": ">=10"
|
|
6806
|
-
}
|
|
6807
|
-
},
|
|
6808
6757
|
"node_modules/ms": {
|
|
6809
6758
|
"version": "2.1.3",
|
|
6810
6759
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
|
@@ -8349,23 +8298,6 @@
|
|
|
8349
8298
|
"url": "https://github.com/sponsors/isaacs"
|
|
8350
8299
|
}
|
|
8351
8300
|
},
|
|
8352
|
-
"node_modules/sirv": {
|
|
8353
|
-
"version": "3.0.2",
|
|
8354
|
-
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
|
8355
|
-
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
|
8356
|
-
"dev": true,
|
|
8357
|
-
"license": "MIT",
|
|
8358
|
-
"optional": true,
|
|
8359
|
-
"peer": true,
|
|
8360
|
-
"dependencies": {
|
|
8361
|
-
"@polka/url": "^1.0.0-next.24",
|
|
8362
|
-
"mrmime": "^2.0.0",
|
|
8363
|
-
"totalist": "^3.0.0"
|
|
8364
|
-
},
|
|
8365
|
-
"engines": {
|
|
8366
|
-
"node": ">=18"
|
|
8367
|
-
}
|
|
8368
|
-
},
|
|
8369
8301
|
"node_modules/slash": {
|
|
8370
8302
|
"version": "3.0.0",
|
|
8371
8303
|
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
|
@@ -8955,18 +8887,6 @@
|
|
|
8955
8887
|
"node": ">=8.0"
|
|
8956
8888
|
}
|
|
8957
8889
|
},
|
|
8958
|
-
"node_modules/totalist": {
|
|
8959
|
-
"version": "3.0.1",
|
|
8960
|
-
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
|
8961
|
-
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
|
8962
|
-
"dev": true,
|
|
8963
|
-
"license": "MIT",
|
|
8964
|
-
"optional": true,
|
|
8965
|
-
"peer": true,
|
|
8966
|
-
"engines": {
|
|
8967
|
-
"node": ">=6"
|
|
8968
|
-
}
|
|
8969
|
-
},
|
|
8970
8890
|
"node_modules/trim-lines": {
|
|
8971
8891
|
"version": "3.0.1",
|
|
8972
8892
|
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
package/template/package.json
CHANGED
|
@@ -24,10 +24,12 @@
|
|
|
24
24
|
"@shikijs/rehype": "^4.3.1",
|
|
25
25
|
"ajv": "^8.12.0",
|
|
26
26
|
"ajv-formats": "^3.0.1",
|
|
27
|
+
"github-slugger": "^2.0.0",
|
|
27
28
|
"gray-matter": "^4.0.3",
|
|
28
29
|
"hast-util-to-string": "^3.0.1",
|
|
29
30
|
"katex": "^0.16.47",
|
|
30
31
|
"lucide-react": "^0.554.0",
|
|
32
|
+
"mdast-util-to-string": "^4.0.0",
|
|
31
33
|
"minisearch": "^7.2.0",
|
|
32
34
|
"next": "^14.2.0",
|
|
33
35
|
"react": "^18.3.0",
|
|
@@ -165,3 +165,77 @@ html.dark .ezw-broken-link {
|
|
|
165
165
|
white-space: normal;
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
|
+
|
|
169
|
+
/* Content pulled in from another page with `![[wiki link]]`.
|
|
170
|
+
Set apart so a reader can tell borrowed text from the page's own, and
|
|
171
|
+
attributed so they can follow it back to where it is maintained. */
|
|
172
|
+
.ezw-transclusion {
|
|
173
|
+
margin: 1.5rem 0;
|
|
174
|
+
padding: 1rem 1.25rem;
|
|
175
|
+
border: 1px solid var(--color-border);
|
|
176
|
+
border-left: 3px solid var(--color-primary, #2563eb);
|
|
177
|
+
border-radius: 0.375rem;
|
|
178
|
+
background: var(--color-sidebar-bg);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.ezw-transclusion > :first-child {
|
|
182
|
+
margin-top: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.ezw-transclusion__source {
|
|
186
|
+
margin: 0.75rem 0 0;
|
|
187
|
+
padding-top: 0.5rem;
|
|
188
|
+
border-top: 1px solid var(--color-border);
|
|
189
|
+
font-size: 0.8125rem;
|
|
190
|
+
color: var(--color-text-muted);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.ezw-transclusion__source::before {
|
|
194
|
+
content: 'From ';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Card shown when a reader hovers or focuses a wiki link. Its contents are
|
|
198
|
+
already on the anchor from the build, so it never waits on a request. */
|
|
199
|
+
.ezw-link-preview {
|
|
200
|
+
position: fixed;
|
|
201
|
+
z-index: 60;
|
|
202
|
+
padding: 0.75rem 0.875rem;
|
|
203
|
+
border: 1px solid var(--color-border);
|
|
204
|
+
border-radius: 0.5rem;
|
|
205
|
+
background: var(--color-background);
|
|
206
|
+
box-shadow:
|
|
207
|
+
0 4px 6px -1px rgb(0 0 0 / 0.1),
|
|
208
|
+
0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
209
|
+
/* Purely informational, so it never intercepts the pointer heading for the
|
|
210
|
+
link underneath it. */
|
|
211
|
+
pointer-events: none;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.ezw-link-preview__title {
|
|
215
|
+
margin: 0;
|
|
216
|
+
font-size: 0.875rem;
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
color: var(--color-text);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.ezw-link-preview__excerpt {
|
|
222
|
+
margin: 0.375rem 0 0;
|
|
223
|
+
font-size: 0.8125rem;
|
|
224
|
+
line-height: 1.5;
|
|
225
|
+
color: var(--color-text-muted);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
229
|
+
.ezw-link-preview {
|
|
230
|
+
animation: ezw-link-preview-in 120ms ease-out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@keyframes ezw-link-preview-in {
|
|
234
|
+
from {
|
|
235
|
+
opacity: 0;
|
|
236
|
+
}
|
|
237
|
+
to {
|
|
238
|
+
opacity: 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|