feat(notes): render code files with Shikiji syntax highlighting
Non-markdown files opened as stacked notes are now highlighted using the existing markdown-it-shikiji pipeline (4-backtick fence wrapping) with a h1 filename heading. Edit controls are hidden for code files. Adds alloy language grammar and a fileLanguage utility mapping extensions to Shikiji language IDs.
This commit is contained in:
@@ -13,7 +13,8 @@ import { useFile } from "@/hooks/useFile.hook"
|
|||||||
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
|
import { useGitHubContent } from "@/hooks/useGitHubContent.hook"
|
||||||
import { useImages } from "@/hooks/useImages.hook"
|
import { useImages } from "@/hooks/useImages.hook"
|
||||||
import { useLinks } from "@/hooks/useLinks.hook"
|
import { useLinks } from "@/hooks/useLinks.hook"
|
||||||
import { runMermaid, useShikiji } from "@/hooks/useMarkdown.hook"
|
import { renderCodeFile, runMermaid, useShikiji } from "@/hooks/useMarkdown.hook"
|
||||||
|
import { getFileLanguage, isMarkdownPath } from "@/utils/fileLanguage"
|
||||||
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
|
import { useNoteOverlay } from "@/hooks/useNoteOverlay.hook"
|
||||||
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
import { useRouteQueryStackedNotes } from "@/hooks/useRouteQueryStackedNotes.hook"
|
||||||
import { useTitleNotes } from "@/hooks/useTitleNotes.hook"
|
import { useTitleNotes } from "@/hooks/useTitleNotes.hook"
|
||||||
@@ -53,6 +54,33 @@ const {
|
|||||||
getEditedSha
|
getEditedSha
|
||||||
} = useFile(sha)
|
} = useFile(sha)
|
||||||
const initialRawContent = ref<string | null>(null)
|
const initialRawContent = ref<string | null>(null)
|
||||||
|
const isMarkdown = computed(() => (path.value ? isMarkdownPath(path.value) : true))
|
||||||
|
const displayedContent = ref("")
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[rawContent, isMarkdown, path],
|
||||||
|
async ([raw, isMd, p]) => {
|
||||||
|
if (!raw) {
|
||||||
|
displayedContent.value = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isMd) {
|
||||||
|
displayedContent.value = content.value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const lang = p ? getFileLanguage(p) : null
|
||||||
|
const filename = p?.split("/").pop()
|
||||||
|
const result = await renderCodeFile(raw, lang, filename)
|
||||||
|
if (rawContent.value === raw) {
|
||||||
|
displayedContent.value = result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(content, (c) => {
|
||||||
|
if (isMarkdown.value) displayedContent.value = c
|
||||||
|
})
|
||||||
const className = computed(() => `stacked-note-${props.index}`)
|
const className = computed(() => `stacked-note-${props.index}`)
|
||||||
const { listenToClick } = useLinks(className.value, sha)
|
const { listenToClick } = useLinks(className.value, sha)
|
||||||
const titleClassName = computed(() => `title-${className.value}`)
|
const titleClassName = computed(() => `title-${className.value}`)
|
||||||
@@ -92,7 +120,7 @@ watch([content, mode], () => {
|
|||||||
runMermaid(`.note-${sha.value} .mermaid`)
|
runMermaid(`.note-${sha.value} .mermaid`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawContent.value.includes("```")) {
|
if (isMarkdown.value && rawContent.value.includes("```")) {
|
||||||
useShikiji()
|
useShikiji()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -157,6 +185,7 @@ watch(mode, async (newMode) => {
|
|||||||
</a>
|
</a>
|
||||||
<section class="text-content">
|
<section class="text-content">
|
||||||
<button
|
<button
|
||||||
|
v-if="isMarkdown"
|
||||||
class="action button is-text is-light"
|
class="action button is-text is-light"
|
||||||
:class="{ 'is-link': mode === 'edit' }"
|
:class="{ 'is-link': mode === 'edit' }"
|
||||||
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
|
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
|
||||||
@@ -205,10 +234,10 @@ watch(mode, async (newMode) => {
|
|||||||
<path d="M14 4l0 4l-6 0l0 -4" />
|
<path d="M14 4l0 4l-6 0l0 -4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="mode === 'edit'" class="edit">
|
<div v-if="mode === 'edit' && isMarkdown" class="edit">
|
||||||
<edit-note v-model="rawContent" />
|
<edit-note v-model="rawContent" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="mode === 'read'" class="note-content" v-html="content"></div>
|
<div v-if="mode === 'read'" class="note-content" v-html="displayedContent"></div>
|
||||||
</section>
|
</section>
|
||||||
<linked-notes v-if="hasBacklinks && content" :sha="sha" />
|
<linked-notes v-if="hasBacklinks && content" :sha="sha" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import markdownItCheckbox from "markdown-it-checkbox"
|
|||||||
import MarkdownItGitHubAlerts from "markdown-it-github-alerts"
|
import MarkdownItGitHubAlerts from "markdown-it-github-alerts"
|
||||||
import markdownItIframe from "markdown-it-iframe"
|
import markdownItIframe from "markdown-it-iframe"
|
||||||
import Shikiji from "markdown-it-shikiji"
|
import Shikiji from "markdown-it-shikiji"
|
||||||
|
import type { LanguageRegistration } from "shikiji-core"
|
||||||
import mermaid from "mermaid"
|
import mermaid from "mermaid"
|
||||||
import { Ref, toValue } from "vue"
|
import { Ref, toValue } from "vue"
|
||||||
|
|
||||||
|
import alloyGrammar from "@/utils/alloy.tmLanguage.json"
|
||||||
import { decodeBase64ToUTF8 } from "@/utils/decodeBase64ToUTF8"
|
import { decodeBase64ToUTF8 } from "@/utils/decodeBase64ToUTF8"
|
||||||
import { html5Media } from "@/utils/markdown/markdown-html5-media"
|
import { html5Media } from "@/utils/markdown/markdown-html5-media"
|
||||||
import { markdownItTablerIcons } from "@/utils/markdown/markdown-it-tabler-icons"
|
import { markdownItTablerIcons } from "@/utils/markdown/markdown-it-tabler-icons"
|
||||||
@@ -116,7 +118,12 @@ export const useShikiji = async () => {
|
|||||||
"mermaid",
|
"mermaid",
|
||||||
"html",
|
"html",
|
||||||
"css",
|
"css",
|
||||||
"json"
|
"json",
|
||||||
|
{
|
||||||
|
...alloyGrammar,
|
||||||
|
name: "alloy",
|
||||||
|
aliases: ["als"]
|
||||||
|
} as unknown as LanguageRegistration
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -157,6 +164,19 @@ const renderMarkdown = (content: string, env?: Record<string, unknown>) => {
|
|||||||
return env ? md.render(content, env) : md.render(content)
|
return env ? md.render(content, env) : md.render(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const renderCodeFile = async (
|
||||||
|
rawContent: string,
|
||||||
|
lang: string | null,
|
||||||
|
filename?: string
|
||||||
|
): Promise<string> => {
|
||||||
|
await useShikiji()
|
||||||
|
const heading = filename ? `# ${filename}\n\n` : ""
|
||||||
|
if (lang !== null) {
|
||||||
|
return renderMarkdown(`${heading}\`\`\`\`${lang}\n${rawContent}\n\`\`\`\``)
|
||||||
|
}
|
||||||
|
return `${renderMarkdown(heading)}<pre><code>${md.utils.escapeHtml(rawContent)}</code></pre>`
|
||||||
|
}
|
||||||
|
|
||||||
export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
|
export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
|
||||||
const getRawContent = (content: string) => decodeBase64ToUTF8(content)
|
const getRawContent = (content: string) => decodeBase64ToUTF8(content)
|
||||||
const renderFromUTF8 = (content: string, prefix?: string) => {
|
const renderFromUTF8 = (content: string, prefix?: string) => {
|
||||||
|
|||||||
244
src/utils/alloy.tmLanguage.json
Normal file
244
src/utils/alloy.tmLanguage.json
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||||
|
"name": "Alloy",
|
||||||
|
"scopeName": "source.als",
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#comments" },
|
||||||
|
{ "include": "#declaration" },
|
||||||
|
{ "include": "#expression" },
|
||||||
|
{ "include": "#built-in" },
|
||||||
|
{ "include": "#keywords" },
|
||||||
|
{ "include": "#digit" }
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"comments": {
|
||||||
|
"patterns": [
|
||||||
|
{ "begin": "/\\*", "end": "\\*/", "name": "comment.block.alloy" },
|
||||||
|
{ "begin": "//", "end": "\n", "name": "comment.line.double-slash" },
|
||||||
|
{ "begin": "--", "end": "\n", "name": "comment.line.double-dash" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"keywords": {
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#define" },
|
||||||
|
{ "include": "#modifier" },
|
||||||
|
{ "include": "#operator" },
|
||||||
|
{ "include": "#control" },
|
||||||
|
{ "include": "#variable" }
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"define": {
|
||||||
|
"patterns": [
|
||||||
|
{ "match": "\\b(sig)\\b", "name": "keyword.language.sig.alloy" },
|
||||||
|
{ "match": "\\b(fact)\\b", "name": "keyword.language.fact.alloy" },
|
||||||
|
{ "match": "\\b(pred)\\b", "name": "keyword.language.pred.alloy" },
|
||||||
|
{ "match": "\\b(fun)\\b", "name": "keyword.language.fun.alloy" },
|
||||||
|
{ "match": "\\b(module)\\b", "name": "keyword.language.module.alloy" },
|
||||||
|
{ "match": "\\b(extends)\\b", "name": "keyword.language.extends.alloy" },
|
||||||
|
{ "match": ":", "name": "keyword.other.colon.alloy" },
|
||||||
|
{ "match": "\\b(check)\\b", "name": "keyword.language.check.alloy" },
|
||||||
|
{ "match": "\\b(assert)\\b", "name": "keyword.language.assert.alloy" },
|
||||||
|
{ "match": "\\b(run)\\b", "name": "keyword.language.run.alloy" },
|
||||||
|
{ "match": "\\b(open)\\b", "name": "keyword.other.open.alloy" },
|
||||||
|
{ "match": "\\b(as)\\b", "name": "keyword.other.as.alloy" },
|
||||||
|
{ "match": "\\b(in)\\b", "name": "keyword.other.in.alloy" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"modifier": {
|
||||||
|
"patterns": [
|
||||||
|
{ "match": "\\b(var)\\b", "name": "keyword.modifier.var.alloy" },
|
||||||
|
{ "match": "\\b(private)\\b", "name": "keyword.modifier.private.alloy" },
|
||||||
|
{ "match": "\\b(abstract)\\b", "name": "keyword.modifier.abstract.alloy" },
|
||||||
|
{ "match": "\\b(all|disj|lone|no|one|set|seq|some|sum|univ|none)\\b", "name": "keyword.modifier.set.alloy" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#temporal" },
|
||||||
|
{ "include": "#unary" },
|
||||||
|
{ "include": "#binary" }
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"temporal": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"match": "\\b(always|eventually|after|before|historically|once|prev)\\b",
|
||||||
|
"name": "keyword.operator.temporal.unary.alloy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "\\b(until|releases|since|triggered)\\b",
|
||||||
|
"name": "keyword.operator.temporal.binary.alloy"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unary": {
|
||||||
|
"patterns": [
|
||||||
|
{ "match": "!|#|~|\\*|\\^|(\\b(not)\\b)", "name": "keyword.operator.unary.alloy" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"binary": {
|
||||||
|
"patterns": [
|
||||||
|
{ "match": "(?:\\|\\|)|&&|<=>|=>|&|\\+|-|\\+\\+|<:|:>|\\.|=|->", "name": "keyword.operator.binary.alloy" },
|
||||||
|
{ "match": "\\b(and|or|iff|implies|else|in)\\b", "name": "keyword.operator.binary.alloy" },
|
||||||
|
{ "match": "=|<|>|=<|>=", "name": "keyword.operator.binary.alloy" },
|
||||||
|
{ "match": ",", "name": "keyword.other.comma.alloy" },
|
||||||
|
{ "match": "\\|", "name": "keyword.other.split.alloy" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"variable": {
|
||||||
|
"patterns": [
|
||||||
|
{ "match": "\\b(let)\\b", "name": "keyword.language.let.alloy" },
|
||||||
|
{ "match": "\\b(this)\\b", "name": "keyword.language.this.alloy" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"control": {
|
||||||
|
"patterns": [
|
||||||
|
{ "match": "\\b(for)\\b", "name": "keyword.control.for.alloy" },
|
||||||
|
{ "match": "\\b(but)\\b", "name": "keyword.control.but.alloy" },
|
||||||
|
{ "match": "\\b(exactly)\\b", "name": "keyword.control.exactly.alloy" },
|
||||||
|
{ "match": "\\b(expect)\\b", "name": "keyword.control.expect.alloy" },
|
||||||
|
{ "match": "\\b(steps)\\b", "name": "keyword.control.steps.alloy" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"patterns": [
|
||||||
|
{ "include": "#module" },
|
||||||
|
{ "include": "#predict" },
|
||||||
|
{ "include": "#signature" },
|
||||||
|
{ "include": "#fact" },
|
||||||
|
{ "include": "#fun" }
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"module": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"match": "(module)\\b\\s*((?:\\w|'|_|\\d|/)+)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "keyword.language.module.alloy" },
|
||||||
|
"2": { "name": "support.class.module.alloy" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"predict": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"match": "(pred)\\b\\s*((?:\\w|'|_|\\d|/)+)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "keyword.language.pred.alloy" },
|
||||||
|
"2": { "name": "entity.name.function.pred.alloy" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"begin": "(abstract)?\\s*(lone|some|one)?\\s*(var)?\\s*(sig)\\b\\s*",
|
||||||
|
"end": "(?=\\{)",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": { "name": "keyword.modifier.abstract.alloy" },
|
||||||
|
"2": { "name": "keyword.modifier.set.alloy" },
|
||||||
|
"3": { "name": "keyword.modifier.var.alloy" },
|
||||||
|
"4": { "name": "keyword.language.sig.alloy" }
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"begin": "(extends)",
|
||||||
|
"end": "(?=\\{)",
|
||||||
|
"beginCaptures": { "1": { "name": "keyword.language.extends.alloy" } },
|
||||||
|
"patterns": [
|
||||||
|
{ "match": "(?:\\w|'|_|\\d|/)+", "name": "entity.other.inherited-class.alloy" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"begin": "(in)",
|
||||||
|
"end": "(?=\\{)",
|
||||||
|
"beginCaptures": { "1": { "name": "keyword.other.in.alloy" } },
|
||||||
|
"patterns": [
|
||||||
|
{ "match": "(?:\\w|'|_|\\d|/)+", "name": "entity.other.inherited-class.alloy" },
|
||||||
|
{ "match": "\\+", "name": "keyword.operator.binary.alloy" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "match": "(?:\\w|'|_|\\d|/)+", "name": "entity.name.type.signature.alloy" },
|
||||||
|
{ "match": ",", "name": "keyword.other.comma.alloy" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fact": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"match": "(fact)\\b\\s*((?:\\w|'|_|\\d|/)+)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "keyword.language.fact.alloy" },
|
||||||
|
"2": { "name": "entity.name.function.fact.alloy" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fun": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"match": "(fun)\\b\\s*((?:\\w|'|_|\\d|/)+)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "keyword.language.fun.alloy" },
|
||||||
|
"2": { "name": "entity.name.function.fun.alloy" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expression": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"match": "(check)\\b\\s*((?:\\w|'|_|\\d|/)+)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "keyword.language.check.alloy" },
|
||||||
|
"2": { "name": "entity.name.function.check.alloy" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "(assert)\\b\\s*((?:\\w|'|_|\\d|/)+)",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "keyword.language.assert.alloy" },
|
||||||
|
"2": { "name": "entity.name.function.check.alloy" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"digit": {
|
||||||
|
"patterns": [
|
||||||
|
{ "match": "\\b(\\d+)\\b", "name": "constant.numeric.alloy" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"built-in": {
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"match": "\\b(plus|minus|mul|div|rem|sum)\\[",
|
||||||
|
"captures": { "1": { "name": "support.function.numeric.alloy" } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "\\b(open)\\b\\s*((?:\\w|'|_|\\d|/)+)\\[",
|
||||||
|
"captures": {
|
||||||
|
"1": { "name": "keyword.other.open.alloy" },
|
||||||
|
"2": { "name": "support.class.module.alloy" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "(/(?:\\w|'|_|\\d|/)+)",
|
||||||
|
"captures": { "1": { "name": "support.function.order.alloy" } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": "((?:\\w|'|_|\\d)+)\\s*\\[",
|
||||||
|
"captures": { "1": { "name": "support.function.order.alloy" } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/utils/fileLanguage.ts
Normal file
31
src/utils/fileLanguage.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const EXT_TO_LANG: Record<string, string> = {
|
||||||
|
sh: "bash",
|
||||||
|
bash: "bash",
|
||||||
|
js: "javascript",
|
||||||
|
mjs: "javascript",
|
||||||
|
cjs: "javascript",
|
||||||
|
ts: "typescript",
|
||||||
|
mts: "typescript",
|
||||||
|
cts: "typescript",
|
||||||
|
md: "markdown",
|
||||||
|
mdx: "markdown",
|
||||||
|
html: "html",
|
||||||
|
htm: "html",
|
||||||
|
css: "css",
|
||||||
|
scss: "css",
|
||||||
|
json: "json",
|
||||||
|
jsonc: "json",
|
||||||
|
als: "alloy"
|
||||||
|
}
|
||||||
|
|
||||||
|
const MARKDOWN_EXTS = new Set(["md", "mdx"])
|
||||||
|
|
||||||
|
export function isMarkdownPath(path: string): boolean {
|
||||||
|
const ext = path.split(".").pop()?.toLowerCase() ?? ""
|
||||||
|
return MARKDOWN_EXTS.has(ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileLanguage(path: string): string | null {
|
||||||
|
const ext = path.split(".").pop()?.toLowerCase() ?? ""
|
||||||
|
return EXT_TO_LANG[ext] ?? null
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user