Added general content blocks
This commit is contained in:
12
src/lib/constants/blocks/notes.ts
Normal file
12
src/lib/constants/blocks/notes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const NOTE_TYPES = [
|
||||
{
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Example',
|
||||
value: 'example',
|
||||
},
|
||||
] as const
|
||||
|
||||
export type CalloutType = (typeof NOTE_TYPES)[number]['value']
|
||||
16
src/lib/constants/blocks/table.ts
Normal file
16
src/lib/constants/blocks/table.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const TABLE_VARIANTS = [
|
||||
{
|
||||
label: 'Default',
|
||||
value: 'default',
|
||||
},
|
||||
{
|
||||
label: 'Random',
|
||||
value: 'random',
|
||||
},
|
||||
{
|
||||
label: 'Icon',
|
||||
value: 'icon',
|
||||
},
|
||||
]
|
||||
|
||||
export type TableVariant = (typeof TABLE_VARIANTS)[number]['value']
|
||||
@@ -2,8 +2,8 @@ import Markdoc from '@markdoc/markdoc'
|
||||
|
||||
import { createDiagnostics } from '../diagnostics'
|
||||
import { createNodes } from './nodes'
|
||||
import { tags } from './tags'
|
||||
import type { NodeState, Renderable, Rendered } from './types'
|
||||
import { createTags } from './tags'
|
||||
import type { NodeState, Renderable, Rendered, ResolveLink } from './types'
|
||||
|
||||
const LEVEL = {
|
||||
critical: 'error',
|
||||
@@ -11,25 +11,37 @@ const LEVEL = {
|
||||
warning: 'warning',
|
||||
} as const
|
||||
|
||||
export async function render({ collection, id, content }: Renderable): Promise<Rendered> {
|
||||
export async function render(
|
||||
{ collection, id, content }: Renderable,
|
||||
resolveLink: ResolveLink,
|
||||
): Promise<Rendered> {
|
||||
const { node } = await content()
|
||||
const state: NodeState = {
|
||||
headings: [],
|
||||
ids: new Set(),
|
||||
footnotes: [],
|
||||
sidenotes: [],
|
||||
markers: new Set(),
|
||||
problems: [],
|
||||
}
|
||||
const config = {
|
||||
nodes: createNodes(state),
|
||||
tags,
|
||||
tags: createTags(state, resolveLink),
|
||||
}
|
||||
const problems = createDiagnostics()
|
||||
|
||||
for (const { lines, error } of Markdoc.validate(node, config)) {
|
||||
const level = LEVEL[error.level as keyof typeof LEVEL]
|
||||
if (level) problems[level](collection, id, `Line ${lines[0] + 1} : ${error.message}`)
|
||||
if (level) problems[level](collection, id, `Text line ${lines[0] + 1}: ${error.message}`)
|
||||
}
|
||||
const tree = Markdoc.transform(node, config)
|
||||
for (const { level, message } of state.problems) problems[level](collection, id, message)
|
||||
|
||||
return {
|
||||
tree: Markdoc.transform(node, config),
|
||||
tree,
|
||||
headings: state.headings,
|
||||
footnotes: state.footnotes,
|
||||
sidenotes: state.sidenotes,
|
||||
diagnostics: problems.list,
|
||||
}
|
||||
}
|
||||
|
||||
17
src/lib/markdoc/notes.ts
Normal file
17
src/lib/markdoc/notes.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Markdoc, { type RenderableTreeNode, type Schema } from '@markdoc/markdoc'
|
||||
|
||||
const unwrap: Schema = { transform: (node, config) => node.transformChildren(config) }
|
||||
|
||||
const INLINE = {
|
||||
nodes: {
|
||||
...Markdoc.nodes,
|
||||
document: unwrap,
|
||||
paragraph: unwrap,
|
||||
},
|
||||
tags: {},
|
||||
}
|
||||
|
||||
export function inlineOf(text: string): RenderableTreeNode[] {
|
||||
const rendered = Markdoc.transform(Markdoc.parse(text), INLINE)
|
||||
return Array.isArray(rendered) ? rendered : [rendered]
|
||||
}
|
||||
@@ -1,12 +1,113 @@
|
||||
import { fields } from '@keystatic/core'
|
||||
|
||||
import Markdoc, { type Node, type Schema } from '@markdoc/markdoc'
|
||||
|
||||
import { generalBlocks } from '@/keystatic/blocks/general'
|
||||
|
||||
export const tags = fields.markdoc.createMarkdocConfig({
|
||||
components: generalBlocks('articles'),
|
||||
render: {
|
||||
tags: {
|
||||
figure: 'Figure',
|
||||
import { ARTICLES } from '../constants/collections'
|
||||
import { inlineOf } from './notes'
|
||||
import type { NodeState, ResolveLink } from './types'
|
||||
|
||||
const { Tag } = Markdoc
|
||||
|
||||
const generated =
|
||||
fields.markdoc.createMarkdocConfig({
|
||||
components: generalBlocks(ARTICLES),
|
||||
render: {
|
||||
tags: {
|
||||
callout: 'Callout',
|
||||
figure: 'Figure',
|
||||
tableWrapper: 'TableWrapper',
|
||||
quote: 'Quote',
|
||||
},
|
||||
},
|
||||
},
|
||||
}).tags
|
||||
}).tags ?? {}
|
||||
|
||||
const text = (value: unknown) => String(value ?? '')
|
||||
|
||||
export function createTags(state: NodeState, resolveLink: ResolveLink): Record<string, Schema> {
|
||||
return {
|
||||
...generated,
|
||||
footnote: {
|
||||
...generated.footnote,
|
||||
transform(node) {
|
||||
const n = state.footnotes.length + 1
|
||||
state.footnotes.push({ n, children: inlineOf(text(node.attributes.note)) })
|
||||
return new Tag('sup', { class: 'footnote-ref' }, [
|
||||
new Tag('a', { href: `#fn-${n}`, id: `fnref-${n}` }, [String(n)]),
|
||||
])
|
||||
},
|
||||
},
|
||||
sidenote: {
|
||||
...generated.sidenote,
|
||||
transform(node, config) {
|
||||
const { marker, type } = node.transformAttributes(config) as Record<string, string>
|
||||
const noteType = type || 'default'
|
||||
if (state.markers.has(marker))
|
||||
state.problems.push({
|
||||
level: 'warning',
|
||||
message: `Two sidenotes use the marker "${marker}".`,
|
||||
})
|
||||
state.markers.add(marker)
|
||||
const n = state.sidenotes.length + 1
|
||||
state.sidenotes.push({
|
||||
n,
|
||||
marker,
|
||||
type: noteType,
|
||||
children: inlineOf(text(node.attributes.note)),
|
||||
})
|
||||
return new Tag(
|
||||
'a',
|
||||
{
|
||||
class: 'sidenote-ref',
|
||||
id: `snref-${n}`,
|
||||
href: `#sn-${n}`,
|
||||
style: `anchor-name: --sn-${n}`,
|
||||
},
|
||||
[marker],
|
||||
)
|
||||
},
|
||||
},
|
||||
xref: {
|
||||
...generated.xref,
|
||||
transform(node: Node, config) {
|
||||
const id = text(node.attributes.reference)
|
||||
const anchor = text(node.attributes.anchor)
|
||||
const children = node.transformChildren(config)
|
||||
const target = resolveLink(id)
|
||||
|
||||
if (!target) {
|
||||
state.problems.push({ level: 'error', message: `Link to "${id}": no such article.` })
|
||||
return children
|
||||
}
|
||||
if (!target.path) {
|
||||
state.problems.push({
|
||||
level: 'warning',
|
||||
message: `Link to "${id}": that article has no page yet, so the link is plain text.`,
|
||||
})
|
||||
return children
|
||||
}
|
||||
return new Tag(
|
||||
'a',
|
||||
{ href: anchor ? `${target.path}#${anchor}` : target.path },
|
||||
children,
|
||||
)
|
||||
},
|
||||
},
|
||||
tableWrapper: {
|
||||
...generated.tableWrapper,
|
||||
validate(node) {
|
||||
const tables = node.children.filter((child) => child.tag === 'table').length
|
||||
return tables === 1
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: 'table-wrapper-children',
|
||||
level: 'warning' as const,
|
||||
message: `A table wrapper should hold exactly one table, this one holds ${tables}.`,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Node, RenderableTreeNode } from '@markdoc/markdoc'
|
||||
|
||||
import type { Diagnostic } from '../diagnostics/types'
|
||||
import type { Diagnostic, Level } from '../diagnostics/types'
|
||||
|
||||
export type Heading = {
|
||||
id: string
|
||||
@@ -8,14 +8,41 @@ export type Heading = {
|
||||
text: string
|
||||
}
|
||||
|
||||
export type Footnote = {
|
||||
n: number
|
||||
children: RenderableTreeNode[]
|
||||
}
|
||||
|
||||
export type Sidenote = {
|
||||
n: number
|
||||
marker: string
|
||||
type: string
|
||||
children: RenderableTreeNode[]
|
||||
}
|
||||
|
||||
export type Problem = {
|
||||
level: Level
|
||||
message: string
|
||||
}
|
||||
|
||||
export type NodeState = {
|
||||
headings: Heading[]
|
||||
ids: Set<string>
|
||||
footnotes: Footnote[]
|
||||
sidenotes: Sidenote[]
|
||||
markers: Set<string>
|
||||
problems: Problem[]
|
||||
}
|
||||
|
||||
export type LinkTarget = { path: string | null } | undefined
|
||||
|
||||
export type ResolveLink = (id: string) => LinkTarget
|
||||
|
||||
export type Rendered = {
|
||||
tree: RenderableTreeNode
|
||||
headings: Heading[]
|
||||
footnotes: Footnote[]
|
||||
sidenotes: Sidenote[]
|
||||
diagnostics: Diagnostic[]
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ export function buildSite(content: Content, visibility: Visibility): Site {
|
||||
pages,
|
||||
tags,
|
||||
byPath,
|
||||
known: new Set(content.articles.keys()),
|
||||
roots,
|
||||
diagnostics: [...content.diagnostics, ...problems.list],
|
||||
}
|
||||
|
||||
10
src/lib/site/links.ts
Normal file
10
src/lib/site/links.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { LinkTarget } from '../markdoc/types'
|
||||
import type { Site } from './types'
|
||||
|
||||
export const linkTo =
|
||||
(site: Site) =>
|
||||
(id: string): LinkTarget => {
|
||||
const article = site.articles.get(id)
|
||||
if (article) return { path: article.path }
|
||||
return site.known.has(id) ? { path: null } : undefined
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export type Site = {
|
||||
pages: Map<string, Page>
|
||||
tags: Map<string, Tag>
|
||||
byPath: Map<string, Routed>
|
||||
known: Set<string>
|
||||
roots: Article[]
|
||||
diagnostics: Diagnostic[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user