mirror of
https://github.com/alrayyes/wiki.git
synced 2024-11-22 03:26:22 +00:00
Merge branch 'v4' of https://github.com/jackyzha0/quartz into v4
This commit is contained in:
commit
923c51de0a
6 changed files with 289 additions and 11 deletions
28
docs/features/Roam Research compatibility.md
Normal file
28
docs/features/Roam Research compatibility.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: "Roam Research Compatibility"
|
||||
tags:
|
||||
- feature/transformer
|
||||
---
|
||||
|
||||
[Roam Research](https://roamresearch.com) is a note-taking tool that organizes your knowledge graph in a unique and interconnected way.
|
||||
|
||||
Quartz supports transforming the special Markdown syntax from Roam Research (like `{{[[components]]}}` and other formatting) into
|
||||
regular Markdown via the [[RoamFlavoredMarkdown]] plugin.
|
||||
|
||||
```typescript title="quartz.config.ts"
|
||||
plugins: {
|
||||
transformers: [
|
||||
// ...
|
||||
Plugin.RoamFlavoredMarkdown(),
|
||||
Plugin.ObsidianFlavoredMarkdown(),
|
||||
// ...
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
> [!warning]
|
||||
> As seen above placement of `Plugin.RoamFlavoredMarkdown()` within `quartz.config.ts` is very important. It must come before `Plugin.ObsidianFlavoredMarkdown()`.
|
||||
|
||||
## Customization
|
||||
|
||||
This functionality is provided by the [[RoamFlavoredMarkdown]] plugin. See the plugin page for customization options.
|
26
docs/plugins/RoamFlavoredMarkdown.md
Normal file
26
docs/plugins/RoamFlavoredMarkdown.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
title: RoamFlavoredMarkdown
|
||||
tags:
|
||||
- plugin/transformer
|
||||
---
|
||||
|
||||
This plugin provides support for [Roam Research](https://roamresearch.com) compatibility. See [[Roam Research Compatibility]] for more information.
|
||||
|
||||
> [!note]
|
||||
> For information on how to add, remove or configure plugins, see the [[Configuration#Plugins|Configuration]] page.
|
||||
|
||||
This plugin accepts the following configuration options:
|
||||
|
||||
- `orComponent`: If `true` (default), converts Roam `{{ or:ONE|TWO|THREE }}` shortcodes into HTML Dropdown options.
|
||||
- `TODOComponent`: If `true` (default), converts Roam `{{[[TODO]]}}` shortcodes into HTML check boxes.
|
||||
- `DONEComponent`: If `true` (default), converts Roam `{{[[DONE]]}}` shortcodes into checked HTML check boxes.
|
||||
- `videoComponent`: If `true` (default), converts Roam `{{[[video]]:URL}}` shortcodes into embeded HTML video.
|
||||
- `audioComponent`: If `true` (default), converts Roam `{{[[audio]]:URL}}` shortcodes into embeded HTML audio.
|
||||
- `pdfComponent`: If `true` (default), converts Roam `{{[[pdf]]:URL}}` shortcodes into embeded HTML PDF viewer.
|
||||
- `blockquoteComponent`: If `true` (default), converts Roam `{{[[>]]}}` shortcodes into Quartz blockquotes.
|
||||
|
||||
## API
|
||||
|
||||
- Category: Transformer
|
||||
- Function name: `Plugin.RoamFlavoredMarkdown()`.
|
||||
- Source: [`quartz/plugins/transformers/roam.ts`](https://github.com/jackyzha0/quartz/blob/v4/quartz/plugins/transformers/roam.ts).
|
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -23,7 +23,7 @@
|
|||
"github-slugger": "^2.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"hast-util-to-html": "^9.0.1",
|
||||
"hast-util-to-html": "^9.0.2",
|
||||
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||
"hast-util-to-string": "^3.0.0",
|
||||
"is-absolute-url": "^4.0.1",
|
||||
|
@ -82,7 +82,7 @@
|
|||
"@types/yargs": "^17.0.33",
|
||||
"esbuild": "^0.19.9",
|
||||
"prettier": "^3.3.3",
|
||||
"tsx": "^4.18.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.4"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2785,15 +2785,14 @@
|
|||
"integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
|
||||
},
|
||||
"node_modules/hast-util-to-html": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.1.tgz",
|
||||
"integrity": "sha512-hZOofyZANbyWo+9RP75xIDV/gq+OUKx+T46IlwERnKmfpwp81XBFbT9mi26ws+SJchA4RVUQwIBJpqEOBhMzEQ==",
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.2.tgz",
|
||||
"integrity": "sha512-RP5wNpj5nm1Z8cloDv4Sl4RS8jH5HYa0v93YB6Wb4poEzgMo/dAAL0KcT4974dCjcNG5pkLqTImeFHHCwwfY3g==",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"hast-util-raw": "^9.0.0",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"html-void-elements": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
|
@ -5836,9 +5835,9 @@
|
|||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.18.0.tgz",
|
||||
"integrity": "sha512-a1jaKBSVQkd6yEc1/NI7G6yHFfefIcuf3QJST7ZEyn4oQnxLYrZR5uZAM8UrwUa3Ge8suiZHcNS1gNrEvmobqg==",
|
||||
"version": "4.19.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.0.tgz",
|
||||
"integrity": "sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.23.0",
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"github-slugger": "^2.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"hast-util-to-html": "^9.0.1",
|
||||
"hast-util-to-html": "^9.0.2",
|
||||
"hast-util-to-jsx-runtime": "^2.3.0",
|
||||
"hast-util-to-string": "^3.0.0",
|
||||
"is-absolute-url": "^4.0.1",
|
||||
|
@ -105,7 +105,7 @@
|
|||
"@types/yargs": "^17.0.33",
|
||||
"esbuild": "^0.19.9",
|
||||
"prettier": "^3.3.3",
|
||||
"tsx": "^4.18.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,3 +10,4 @@ export { OxHugoFlavouredMarkdown } from "./oxhugofm"
|
|||
export { SyntaxHighlighting } from "./syntax"
|
||||
export { TableOfContents } from "./toc"
|
||||
export { HardLineBreaks } from "./linebreaks"
|
||||
export { RoamFlavoredMarkdown } from "./roam"
|
||||
|
|
224
quartz/plugins/transformers/roam.ts
Normal file
224
quartz/plugins/transformers/roam.ts
Normal file
|
@ -0,0 +1,224 @@
|
|||
import { QuartzTransformerPlugin } from "../types"
|
||||
import { PluggableList } from "unified"
|
||||
import { SKIP, visit } from "unist-util-visit"
|
||||
import { ReplaceFunction, findAndReplace as mdastFindReplace } from "mdast-util-find-and-replace"
|
||||
import { Root, Html, Paragraph, Text, Link, Parent } from "mdast"
|
||||
import { Node } from "unist"
|
||||
import { VFile } from "vfile"
|
||||
import { BuildVisitor } from "unist-util-visit"
|
||||
|
||||
export interface Options {
|
||||
orComponent: boolean
|
||||
TODOComponent: boolean
|
||||
DONEComponent: boolean
|
||||
videoComponent: boolean
|
||||
audioComponent: boolean
|
||||
pdfComponent: boolean
|
||||
blockquoteComponent: boolean
|
||||
tableComponent: boolean
|
||||
attributeComponent: boolean
|
||||
}
|
||||
|
||||
const defaultOptions: Options = {
|
||||
orComponent: true,
|
||||
TODOComponent: true,
|
||||
DONEComponent: true,
|
||||
videoComponent: true,
|
||||
audioComponent: true,
|
||||
pdfComponent: true,
|
||||
blockquoteComponent: true,
|
||||
tableComponent: true,
|
||||
attributeComponent: true,
|
||||
}
|
||||
|
||||
const orRegex = new RegExp(/{{or:(.*?)}}/, "g")
|
||||
const TODORegex = new RegExp(/{{.*?\bTODO\b.*?}}/, "g")
|
||||
const DONERegex = new RegExp(/{{.*?\bDONE\b.*?}}/, "g")
|
||||
const videoRegex = new RegExp(/{{.*?\[\[video\]\].*?\:(.*?)}}/, "g")
|
||||
const youtubeRegex = new RegExp(
|
||||
/{{.*?\[\[video\]\].*?(https?:\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?)}}/,
|
||||
"g",
|
||||
)
|
||||
|
||||
// const multimediaRegex = new RegExp(/{{.*?\b(video|audio)\b.*?\:(.*?)}}/, "g")
|
||||
|
||||
const audioRegex = new RegExp(/{{.*?\[\[audio\]\].*?\:(.*?)}}/, "g")
|
||||
const pdfRegex = new RegExp(/{{.*?\[\[pdf\]\].*?\:(.*?)}}/, "g")
|
||||
const blockquoteRegex = new RegExp(/(\[\[>\]\])\s*(.*)/, "g")
|
||||
const roamHighlightRegex = new RegExp(/\^\^(.+)\^\^/, "g")
|
||||
const roamItalicRegex = new RegExp(/__(.+)__/, "g")
|
||||
const tableRegex = new RegExp(/- {{.*?\btable\b.*?}}/, "g") /* TODO */
|
||||
const attributeRegex = new RegExp(/\b\w+(?:\s+\w+)*::/, "g") /* TODO */
|
||||
|
||||
function isSpecialEmbed(node: Paragraph): boolean {
|
||||
if (node.children.length !== 2) return false
|
||||
|
||||
const [textNode, linkNode] = node.children
|
||||
return (
|
||||
textNode.type === "text" &&
|
||||
textNode.value.startsWith("{{[[") &&
|
||||
linkNode.type === "link" &&
|
||||
linkNode.children[0].type === "text" &&
|
||||
linkNode.children[0].value.endsWith("}}")
|
||||
)
|
||||
}
|
||||
|
||||
function transformSpecialEmbed(node: Paragraph, opts: Options): Html | null {
|
||||
const [textNode, linkNode] = node.children as [Text, Link]
|
||||
const embedType = textNode.value.match(/\{\{\[\[(.*?)\]\]:/)?.[1]?.toLowerCase()
|
||||
const url = linkNode.url.slice(0, -2) // Remove the trailing '}}'
|
||||
|
||||
switch (embedType) {
|
||||
case "audio":
|
||||
return opts.audioComponent
|
||||
? {
|
||||
type: "html",
|
||||
value: `<audio controls>
|
||||
<source src="${url}" type="audio/mpeg">
|
||||
<source src="${url}" type="audio/ogg">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>`,
|
||||
}
|
||||
: null
|
||||
case "video":
|
||||
if (!opts.videoComponent) return null
|
||||
// Check if it's a YouTube video
|
||||
const youtubeMatch = url.match(
|
||||
/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?(.+)/,
|
||||
)
|
||||
if (youtubeMatch) {
|
||||
const videoId = youtubeMatch[1].split("&")[0] // Remove additional parameters
|
||||
const playlistMatch = url.match(/[?&]list=([^#\&\?]*)/)
|
||||
const playlistId = playlistMatch ? playlistMatch[1] : null
|
||||
|
||||
return {
|
||||
type: "html",
|
||||
value: `<iframe
|
||||
class="external-embed youtube"
|
||||
width="600px"
|
||||
height="350px"
|
||||
src="https://www.youtube.com/embed/${videoId}${playlistId ? `?list=${playlistId}` : ""}"
|
||||
frameborder="0"
|
||||
allow="fullscreen"
|
||||
></iframe>`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: "html",
|
||||
value: `<video controls>
|
||||
<source src="${url}" type="video/mp4">
|
||||
<source src="${url}" type="video/webm">
|
||||
Your browser does not support the video tag.
|
||||
</video>`,
|
||||
}
|
||||
}
|
||||
case "pdf":
|
||||
return opts.pdfComponent
|
||||
? {
|
||||
type: "html",
|
||||
value: `<embed src="${url}" type="application/pdf" width="100%" height="600px" />`,
|
||||
}
|
||||
: null
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const RoamFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options> | undefined> = (
|
||||
userOpts,
|
||||
) => {
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
|
||||
return {
|
||||
name: "RoamFlavoredMarkdown",
|
||||
markdownPlugins() {
|
||||
const plugins: PluggableList = []
|
||||
|
||||
plugins.push(() => {
|
||||
return (tree: Root, file: VFile) => {
|
||||
const replacements: [RegExp, ReplaceFunction][] = []
|
||||
|
||||
// Handle special embeds (audio, video, PDF)
|
||||
if (opts.audioComponent || opts.videoComponent || opts.pdfComponent) {
|
||||
visit(tree, "paragraph", ((node: Paragraph, index: number, parent: Parent | null) => {
|
||||
if (isSpecialEmbed(node)) {
|
||||
const transformedNode = transformSpecialEmbed(node, opts)
|
||||
if (transformedNode && parent) {
|
||||
parent.children[index] = transformedNode
|
||||
}
|
||||
}
|
||||
}) as BuildVisitor<Root, "paragraph">)
|
||||
}
|
||||
|
||||
// Roam italic syntax
|
||||
replacements.push([
|
||||
roamItalicRegex,
|
||||
(_value: string, match: string) => ({
|
||||
type: "emphasis",
|
||||
children: [{ type: "text", value: match }],
|
||||
}),
|
||||
])
|
||||
|
||||
// Roam highlight syntax
|
||||
replacements.push([
|
||||
roamHighlightRegex,
|
||||
(_value: string, inner: string) => ({
|
||||
type: "html",
|
||||
value: `<span class="text-highlight">${inner}</span>`,
|
||||
}),
|
||||
])
|
||||
|
||||
if (opts.orComponent) {
|
||||
replacements.push([
|
||||
orRegex,
|
||||
(match: string) => {
|
||||
const matchResult = match.match(/{{or:(.*?)}}/)
|
||||
if (matchResult === null) {
|
||||
return { type: "html", value: "" }
|
||||
}
|
||||
const optionsString: string = matchResult[1]
|
||||
const options: string[] = optionsString.split("|")
|
||||
const selectHtml: string = `<select>${options.map((option: string) => `<option value="${option}">${option}</option>`).join("")}</select>`
|
||||
return { type: "html", value: selectHtml }
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
if (opts.TODOComponent) {
|
||||
replacements.push([
|
||||
TODORegex,
|
||||
() => ({
|
||||
type: "html",
|
||||
value: `<input type="checkbox" disabled>`,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
if (opts.DONEComponent) {
|
||||
replacements.push([
|
||||
DONERegex,
|
||||
() => ({
|
||||
type: "html",
|
||||
value: `<input type="checkbox" checked disabled>`,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
if (opts.blockquoteComponent) {
|
||||
replacements.push([
|
||||
blockquoteRegex,
|
||||
(_match: string, _marker: string, content: string) => ({
|
||||
type: "html",
|
||||
value: `<blockquote>${content.trim()}</blockquote>`,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
mdastFindReplace(tree, replacements)
|
||||
}
|
||||
})
|
||||
|
||||
return plugins
|
||||
},
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue