cantip 0.1.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/LICENSE +21 -0
- package/README.md +61 -0
- package/app/components/CanvasMount.tsx +62 -0
- package/app/components/CodeWrapToggle.tsx +78 -0
- package/app/components/FindOnPage.tsx +224 -0
- package/app/components/MobileBottomBar.tsx +93 -0
- package/app/components/MobileProjectsPanel.tsx +113 -0
- package/app/components/PageFloatingMenu.tsx +224 -0
- package/app/components/ProjectSwitcher.tsx +124 -0
- package/app/components/Search.tsx +930 -0
- package/app/components/ShortcutsHelp.tsx +113 -0
- package/app/components/Sidebar.tsx +1049 -0
- package/app/components/TabBar.tsx +227 -0
- package/app/components/Toc.tsx +129 -0
- package/app/components/TopBar.tsx +74 -0
- package/app/components/theme-toggle.tsx +71 -0
- package/app/components/ui/button.tsx +56 -0
- package/app/components/ui/card.tsx +55 -0
- package/app/components/ui/dropdown-menu.tsx +156 -0
- package/app/components/ui/input.tsx +21 -0
- package/app/entry.client.tsx +12 -0
- package/app/entry.server.tsx +155 -0
- package/app/generated/site.ts +19 -0
- package/app/generated/slots.ts +10 -0
- package/app/generated/theme.generated.css +60 -0
- package/app/lib/config/config.server.ts +50 -0
- package/app/lib/config/defaults.ts +120 -0
- package/app/lib/config/load.ts +82 -0
- package/app/lib/config/schema.ts +131 -0
- package/app/lib/config/site.ts +43 -0
- package/app/lib/content.server.ts +105 -0
- package/app/lib/projects.ts +86 -0
- package/app/lib/sidebar.server.ts +113 -0
- package/app/lib/site.ts +27 -0
- package/app/lib/slots.tsx +33 -0
- package/app/lib/tabs.tsx +128 -0
- package/app/lib/useKeyboardShortcuts.ts +149 -0
- package/app/lib/utils.ts +17 -0
- package/app/root.tsx +171 -0
- package/app/routes/$.tsx +158 -0
- package/app/routes/_index.tsx +60 -0
- package/app/styles/app.css +461 -0
- package/app/styles/obsidian.css +83 -0
- package/app/styles/tailwind.css +227 -0
- package/cli.js +119 -0
- package/components.json +21 -0
- package/dist/config.mjs +87 -0
- package/dist/generate-content.mjs +1665 -0
- package/package.json +112 -0
- package/scripts/build-search-index.ts +129 -0
- package/scripts/canonical.ts +34 -0
- package/scripts/canvas-to-md.ts +73 -0
- package/scripts/compile.ts +242 -0
- package/scripts/emit-config.ts +163 -0
- package/scripts/generate-content.ts +197 -0
- package/scripts/obsidian/files.ts +222 -0
- package/scripts/obsidian/fs.ts +34 -0
- package/scripts/obsidian/generate.ts +36 -0
- package/scripts/obsidian/html.ts +17 -0
- package/scripts/obsidian/logger.ts +10 -0
- package/scripts/obsidian/markdown.ts +56 -0
- package/scripts/obsidian/obsidian.ts +229 -0
- package/scripts/obsidian/path.ts +60 -0
- package/scripts/obsidian/rehype.ts +60 -0
- package/scripts/obsidian/remark.ts +712 -0
- package/scripts/obsidian/types.ts +31 -0
- package/vite.config.ts +62 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
2
|
+
import { remark } from 'remark'
|
|
3
|
+
import remarkFrontmatter from 'remark-frontmatter'
|
|
4
|
+
import remarkGfm from 'remark-gfm'
|
|
5
|
+
import remarkMath from 'remark-math'
|
|
6
|
+
import { VFile } from 'vfile'
|
|
7
|
+
|
|
8
|
+
import { remarkObsidian, type TransformContext } from './remark.ts'
|
|
9
|
+
|
|
10
|
+
let processor: ReturnType<typeof remark> | undefined
|
|
11
|
+
|
|
12
|
+
export async function transformMarkdownToString(
|
|
13
|
+
filePath: string,
|
|
14
|
+
markdown: string,
|
|
15
|
+
context: TransformContext,
|
|
16
|
+
): Promise<TransformResult> {
|
|
17
|
+
const file = await getProcessor(context).process(getVFile(filePath, markdown, context))
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
aliases: file.data.aliases,
|
|
21
|
+
content: String(file),
|
|
22
|
+
skip: file.data.skip === true,
|
|
23
|
+
type: file.data.isMdx === true ? 'mdx' : 'markdown',
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function transformMarkdownToAST(filePath: string, markdown: string, context: TransformContext) {
|
|
28
|
+
const { content } = await transformMarkdownToString(filePath, markdown, context)
|
|
29
|
+
return fromMarkdown(content)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getProcessor(context: TransformContext): ReturnType<typeof remark> {
|
|
33
|
+
processor ??= remark()
|
|
34
|
+
.data('settings', { resourceLink: true })
|
|
35
|
+
.use(remarkGfm)
|
|
36
|
+
.use(remarkMath, { singleDollarTextMath: context.singleDollarTextMath })
|
|
37
|
+
.use(remarkFrontmatter)
|
|
38
|
+
.use(remarkObsidian)
|
|
39
|
+
|
|
40
|
+
return processor
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getVFile(filePath: string, markdown: string, context: TransformContext) {
|
|
44
|
+
return new VFile({
|
|
45
|
+
data: { ...context },
|
|
46
|
+
path: filePath,
|
|
47
|
+
value: markdown,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface TransformResult {
|
|
52
|
+
aliases: string[] | undefined
|
|
53
|
+
content: string
|
|
54
|
+
skip: boolean
|
|
55
|
+
type: 'markdown' | 'mdx'
|
|
56
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import decodeUriComponent from 'decode-uri-component'
|
|
6
|
+
import { slug } from 'github-slugger'
|
|
7
|
+
import { globby } from 'globby'
|
|
8
|
+
import yaml from 'yaml'
|
|
9
|
+
|
|
10
|
+
import type { ObsidianConfig } from './types.ts'
|
|
11
|
+
|
|
12
|
+
import { isDirectory, isFile } from './fs.ts'
|
|
13
|
+
import { getExtension, isAnchor, slashify, slugifyPath, stripExtension } from './path.ts'
|
|
14
|
+
import { isAssetFile } from './files.ts'
|
|
15
|
+
|
|
16
|
+
const obsidianAppConfigSchema = z.object({
|
|
17
|
+
newLinkFormat: z.union([z.literal('absolute'), z.literal('relative'), z.literal('shortest')]).default('shortest'),
|
|
18
|
+
useMarkdownLinks: z.boolean().default(false),
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const obsidianFrontmatterSchema = z.object({
|
|
22
|
+
aliases: z
|
|
23
|
+
.array(z.string())
|
|
24
|
+
.optional()
|
|
25
|
+
.nullable()
|
|
26
|
+
.transform((aliases) => aliases?.map((alias) => slug(alias))),
|
|
27
|
+
cover: z.string().optional().nullable(),
|
|
28
|
+
description: z.string().optional().nullable(),
|
|
29
|
+
image: z.string().optional().nullable(),
|
|
30
|
+
permalink: z.string().optional().nullable(),
|
|
31
|
+
publish: z
|
|
32
|
+
.union([z.boolean(), z.literal('true'), z.literal('false')])
|
|
33
|
+
.optional()
|
|
34
|
+
.nullable()
|
|
35
|
+
.transform((publish) => publish === undefined || publish === 'true' || publish === true),
|
|
36
|
+
tags: z.array(z.string()).optional().nullable(),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const imageFileFormats = new Set(['.avif', '.bmp', '.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp'])
|
|
40
|
+
const audioFileFormats = new Set(['.flac', '.m4a', '.mp3', '.wav', '.ogg', '.wav', '.3gp'])
|
|
41
|
+
const videoFileFormats = new Set(['.mkv', '.mov', '.mp4', '.ogv', '.webm'])
|
|
42
|
+
const otherFileFormats = new Set(['.pdf'])
|
|
43
|
+
|
|
44
|
+
const fileFormats = new Set([...imageFileFormats, ...audioFileFormats, ...videoFileFormats, ...otherFileFormats])
|
|
45
|
+
|
|
46
|
+
export async function getVault(config: ObsidianConfig): Promise<Vault> {
|
|
47
|
+
const vaultPath = path.resolve(config.vault)
|
|
48
|
+
|
|
49
|
+
if (!(await isDirectory(vaultPath))) {
|
|
50
|
+
throw new Error(`The provided vault path is not a directory.\n> Provided path: ${vaultPath}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A real Obsidian vault carries `.obsidian/app.json`; a plain "loose content
|
|
54
|
+
// folder" (a supported input mode) does not. For the latter, fall back to
|
|
55
|
+
// Obsidian's own defaults (shortest wikilinks) instead of erroring, so users
|
|
56
|
+
// can point a project `source` at any directory of markdown.
|
|
57
|
+
const options = (await isVaultDirectory(config, vaultPath))
|
|
58
|
+
? await getVaultOptions(config, vaultPath)
|
|
59
|
+
: { linkFormat: 'shortest' as const, linkSyntax: 'wikilink' as const }
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
options,
|
|
63
|
+
path: slashify(vaultPath),
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getObsidianPaths(vault: Vault, ignore: ObsidianConfig['ignore'] = []) {
|
|
68
|
+
return globby(['**/*.md', ...[...fileFormats].map((fileFormat) => `**/*${fileFormat}`)], {
|
|
69
|
+
absolute: true,
|
|
70
|
+
cwd: vault.path,
|
|
71
|
+
ignore,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getObsidianVaultFiles(vault: Vault, obsidianPaths: string[]): VaultFile[] {
|
|
76
|
+
const allFileNames = obsidianPaths.map((obsidianPath) => path.basename(obsidianPath))
|
|
77
|
+
|
|
78
|
+
return obsidianPaths.map((obsidianPath, index) => {
|
|
79
|
+
const baseFileName = allFileNames[index] as string
|
|
80
|
+
let fileName = baseFileName
|
|
81
|
+
|
|
82
|
+
const type = isAssetFile(fileName) ? 'asset' : isObsidianFile(fileName) ? 'file' : 'content'
|
|
83
|
+
|
|
84
|
+
if (type === 'asset') {
|
|
85
|
+
fileName = slugifyPath(fileName)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const filePath = getObsidianRelativePath(vault, obsidianPath)
|
|
89
|
+
const fileSlug = slugifyObsidianPath(filePath)
|
|
90
|
+
|
|
91
|
+
return createVaultFile({
|
|
92
|
+
fileName,
|
|
93
|
+
fsPath: obsidianPath,
|
|
94
|
+
path: type === 'asset' ? fileSlug : filePath,
|
|
95
|
+
slug: fileSlug,
|
|
96
|
+
stem: stripExtension(fileName),
|
|
97
|
+
type,
|
|
98
|
+
uniqueFileName: allFileNames.filter((currentFileName) => currentFileName === baseFileName).length === 1,
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getObsidianRelativePath(vault: Vault, obsidianPath: string) {
|
|
104
|
+
return obsidianPath.replace(vault.path, '')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function slugifyObsidianPath(obsidianPath: string) {
|
|
108
|
+
const segments = obsidianPath.split('/')
|
|
109
|
+
|
|
110
|
+
return segments
|
|
111
|
+
.map((segment, index) => {
|
|
112
|
+
const isLastSegment = index === segments.length - 1
|
|
113
|
+
|
|
114
|
+
if (!isLastSegment) {
|
|
115
|
+
return slug(decodeUriComponent(segment))
|
|
116
|
+
} else if (isObsidianFile(segment) && !isAssetFile(segment)) {
|
|
117
|
+
return decodeUriComponent(segment)
|
|
118
|
+
} else if (isAssetFile(segment)) {
|
|
119
|
+
return `${slug(decodeUriComponent(stripExtension(segment)))}${getExtension(segment)}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return slug(decodeUriComponent(stripExtension(segment)))
|
|
123
|
+
})
|
|
124
|
+
.join('/')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function slugifyObsidianAnchor(obsidianAnchor: string) {
|
|
128
|
+
if (obsidianAnchor.length === 0) {
|
|
129
|
+
return ''
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let anchor = isAnchor(obsidianAnchor) ? obsidianAnchor.slice(1) : obsidianAnchor
|
|
133
|
+
|
|
134
|
+
if (isObsidianBlockAnchor(anchor)) {
|
|
135
|
+
anchor = anchor.replace('^', 'block-')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return `#${slug(decodeURIComponent(anchor))}`
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function isObsidianBlockAnchor(anchor: string) {
|
|
142
|
+
return anchor.startsWith('#^') || anchor.startsWith('^')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isObsidianFile(filePath: string, type?: 'image' | 'audio' | 'video' | 'other') {
|
|
146
|
+
const formats: Set<string> =
|
|
147
|
+
type === undefined
|
|
148
|
+
? fileFormats
|
|
149
|
+
: type === 'image'
|
|
150
|
+
? imageFileFormats
|
|
151
|
+
: type === 'audio'
|
|
152
|
+
? audioFileFormats
|
|
153
|
+
: type === 'video'
|
|
154
|
+
? videoFileFormats
|
|
155
|
+
: otherFileFormats
|
|
156
|
+
|
|
157
|
+
return formats.has(getExtension(filePath))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function parseObsidianFrontmatter(content: string): ObsidianFrontmatter | undefined {
|
|
161
|
+
try {
|
|
162
|
+
const raw: unknown = yaml.parse(content)
|
|
163
|
+
return { ...obsidianFrontmatterSchema.parse(raw), raw: raw as ObsidianFrontmatter['raw'] }
|
|
164
|
+
} catch {
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createVaultFile(baseVaultFile: BaseVaultFile) {
|
|
170
|
+
return {
|
|
171
|
+
...baseVaultFile,
|
|
172
|
+
isEqualFileName(otherFileName: string) {
|
|
173
|
+
return (isAssetFile(otherFileName) ? slugifyPath(otherFileName) : otherFileName) === this.fileName
|
|
174
|
+
},
|
|
175
|
+
isEqualStem(otherStem: string) {
|
|
176
|
+
return (isAssetFile(otherStem) ? slugifyPath(otherStem) : otherStem) === this.stem
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function isVaultDirectory(config: ObsidianConfig, vaultPath: string) {
|
|
182
|
+
const configPath = path.join(vaultPath, config.configFolder)
|
|
183
|
+
return (await isDirectory(configPath)) && (await isFile(path.join(configPath, 'app.json')))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function getVaultOptions(config: ObsidianConfig, vaultPath: string): Promise<VaultOptions> {
|
|
187
|
+
const appConfigPath = path.join(vaultPath, config.configFolder, 'app.json')
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const appConfigData = await fs.readFile(appConfigPath, 'utf8')
|
|
191
|
+
const appConfig = obsidianAppConfigSchema.parse(JSON.parse(appConfigData))
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
linkFormat: appConfig.newLinkFormat,
|
|
195
|
+
linkSyntax: appConfig.useMarkdownLinks ? 'markdown' : 'wikilink',
|
|
196
|
+
}
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw new Error('Failed to read Obsidian vault app configuration.', { cause: error })
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface Vault {
|
|
203
|
+
options: VaultOptions
|
|
204
|
+
path: string
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface VaultOptions {
|
|
208
|
+
linkFormat: 'absolute' | 'relative' | 'shortest'
|
|
209
|
+
linkSyntax: 'markdown' | 'wikilink'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface BaseVaultFile {
|
|
213
|
+
fileName: string
|
|
214
|
+
fsPath: string
|
|
215
|
+
path: string
|
|
216
|
+
slug: string
|
|
217
|
+
stem: string
|
|
218
|
+
type: 'asset' | 'content' | 'file'
|
|
219
|
+
uniqueFileName: boolean
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface VaultFile extends BaseVaultFile {
|
|
223
|
+
isEqualFileName: (otherFileName: string) => boolean
|
|
224
|
+
isEqualStem: (otherStem: string) => boolean
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export type ObsidianFrontmatter = z.output<typeof obsidianFrontmatterSchema> & {
|
|
228
|
+
raw: Record<string | number, unknown>
|
|
229
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { slug } from 'github-slugger'
|
|
4
|
+
|
|
5
|
+
export function getExtension(filePath: string) {
|
|
6
|
+
return path.parse(filePath).ext
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function stripExtension(filePath: string) {
|
|
10
|
+
return path.parse(filePath).name
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function extractPathAndAnchor(filePathAndAnchor: string): [string, string | undefined] {
|
|
14
|
+
const [filePath, fileAnchor] = filePathAndAnchor.split('#', 2)
|
|
15
|
+
return [filePath as string, fileAnchor]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isAnchor(filePath: string): filePath is `#${string}` {
|
|
19
|
+
return filePath.startsWith('#')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function slugifyPath(filePath: string) {
|
|
23
|
+
const segments = filePath.split('/')
|
|
24
|
+
return segments
|
|
25
|
+
.map((segment, index) => {
|
|
26
|
+
const isLastSegment = index === segments.length - 1
|
|
27
|
+
if (!isLastSegment) {
|
|
28
|
+
return slug(segment)
|
|
29
|
+
}
|
|
30
|
+
const parsedPath = path.parse(segment)
|
|
31
|
+
return `${slug(parsedPath.name)}${parsedPath.ext}`
|
|
32
|
+
})
|
|
33
|
+
.join('/')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function slashify(filePath: string) {
|
|
37
|
+
const isExtendedLengthPath = filePath.startsWith('\\\\?\\')
|
|
38
|
+
if (isExtendedLengthPath) {
|
|
39
|
+
return filePath
|
|
40
|
+
}
|
|
41
|
+
return filePath.replaceAll('\\', '/')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function osPath(filePath: string) {
|
|
45
|
+
return filePath.replaceAll('/', path.sep)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stripLeadingSlash(href: string) {
|
|
49
|
+
if (href.startsWith('/')) href = href.slice(1)
|
|
50
|
+
return href
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stripTrailingSlash(href: string) {
|
|
54
|
+
if (href.endsWith('/')) href = href.slice(0, -1)
|
|
55
|
+
return href
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function stripLeadingAndTrailingSlashes(href: string): string {
|
|
59
|
+
return stripTrailingSlash(stripLeadingSlash(href))
|
|
60
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Element, ElementContent, Root } from 'hast'
|
|
2
|
+
import type { Literal } from 'mdast'
|
|
3
|
+
import { CONTINUE, SKIP, visit } from 'unist-util-visit'
|
|
4
|
+
|
|
5
|
+
const blockIdentifierRegex = /(?<identifier> *\^(?<name>[\w-]+))$/
|
|
6
|
+
|
|
7
|
+
export function rehypeObsidian() {
|
|
8
|
+
return function transformer(tree: Root) {
|
|
9
|
+
visit(tree, 'element', (node) => {
|
|
10
|
+
if (node.tagName === 'blockquote') {
|
|
11
|
+
const lastChild = node.children.at(-1)
|
|
12
|
+
if (
|
|
13
|
+
lastChild?.type !== 'element' ||
|
|
14
|
+
!(lastChild.tagName === 'p' || lastChild.tagName === 'ul' || lastChild.tagName === 'ol')
|
|
15
|
+
) {
|
|
16
|
+
return CONTINUE
|
|
17
|
+
}
|
|
18
|
+
const lastGrandChild = lastChild.children.at(-1)
|
|
19
|
+
if (lastChild.tagName === 'p') {
|
|
20
|
+
return transformBlockIdentifier(node, lastGrandChild)
|
|
21
|
+
} else if (lastGrandChild?.type === 'element' && lastGrandChild.tagName === 'li') {
|
|
22
|
+
return transformBlockIdentifier(node, lastGrandChild.children.at(-1))
|
|
23
|
+
}
|
|
24
|
+
} else if (node.tagName === 'p' || node.tagName === 'li') {
|
|
25
|
+
return transformBlockIdentifier(node, node.children.at(-1))
|
|
26
|
+
}
|
|
27
|
+
return CONTINUE
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function transformBlockIdentifier(reference: Element, node: ElementContent | undefined) {
|
|
33
|
+
if (!isNodeWithValue(node)) {
|
|
34
|
+
return CONTINUE
|
|
35
|
+
}
|
|
36
|
+
const identifier = getBlockIdentifer(node)
|
|
37
|
+
if (!identifier) {
|
|
38
|
+
return CONTINUE
|
|
39
|
+
}
|
|
40
|
+
node.value = node.value.slice(0, identifier.length * -1)
|
|
41
|
+
reference.properties = reference.properties ?? {}
|
|
42
|
+
reference.properties['id'] = `block-${identifier.name}`
|
|
43
|
+
return SKIP
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isNodeWithValue(node: ElementContent | undefined): node is NodeWithValue {
|
|
47
|
+
return node !== undefined && 'value' in node
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getBlockIdentifer(node: NodeWithValue): { length: number; name: string } | undefined {
|
|
51
|
+
const match = blockIdentifierRegex.exec(node.value)
|
|
52
|
+
const identifier = match?.groups?.['identifier']
|
|
53
|
+
const name = match?.groups?.['name']
|
|
54
|
+
if (!identifier || !name) {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
return { length: identifier.length, name }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type NodeWithValue = ElementContent & Literal
|