All files / markdown / plugins / directives.ts

90.00% Branches 18/20
100.00% Functions 3/3
91.84% Lines 45/49
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x20
x3
x6
x6
x6
x6
 
 
x3
x6
x6
x3
x3
x3
x6
 
 
x3
x3
x3
x6
x6
x3
x3
x3
x3
x3
x3
x3
x3
x3
 
 
x20
x3
x3
x4
x4
x1
x1
x4
x1
x1
x4
x2
x2
x2
x4
x4
x3
x3
































I








I

































// Imports
import type { MarkdownIt } from "../types.ts"

/** Directives options. */
export type DirectivesOptions = {
  /** Indicate how to render a directive (defaults to a `<div>` with the directive name as class). */
  render?: (name: string, attributes: Record<string, string>) => { tag: string; attributes?: Record<string, string> }
}

/**
 * Add support for custom container directives.
 *
 * Directives may be assigned attributes using the `{#id .class key=value}` syntax.
 * Use the `render` option to customize the rendered tag and attributes.
 *
 * ```md
 * :::foo{bar=qux}
 * baz
 * :::
 * ```
 * ```html
 * <div class="foo" bar="qux">
 *   <p>baz</p>
 * </div>
 * ```
 */
export default function directives(engine: MarkdownIt, { render = (name, attributes) => ({ tag: "div", attributes: { ...attributes, class: [name, attributes.class].filter(Boolean).join(" ") } }) }: DirectivesOptions = {}): void {
  engine.block.ruler.before("fence", "directives", (state, startLine, endLine, silent) => {
    const line = state.src.slice(state.bMarks[startLine] + state.tShift[startLine], state.eMarks[startLine])
    const match = /^:::([\w-]+)(?:\{(.*?)\})?\s*$/.exec(line)
    if (!match)
      return false
    if (silent)
      return true
    let close = -1
    for (let next = startLine + 1; next < endLine; next++) {
      if (state.src.slice(state.bMarks[next] + state.tShift[next], state.eMarks[next]).trim() === ":::") {
        close = next
        break
      }
    }
    if (close < 0)
      return false
    const { tag, attributes = {} } = render(match[1], parseAttributes(match[2]))
    const open = state.push("directive_open", tag, 1)
    open.block = true
    for (const [attribute, value] of Object.entries(attributes))
      open.attrSet(attribute, value)
    const max = state.lineMax
    state.lineMax = close
    state.md.block.tokenize(state, startLine + 1, close)
    state.lineMax = max
    state.push("directive_close", tag, -1).block = true
    state.line = close + 1
    return true
  })
}

/** Parse directive attributes (`{#id .class key=value}`). */
function parseAttributes(text = ""): Record<string, string> {
  const attributes = {} as Record<string, string>
  for (const attribute of text.match(/[.#][\w-]+|[\w-]+="[^"]*"|[\w-]+=\S+/g) ?? []) {
    switch (true) {
      case attribute.startsWith("."):
        attributes.class = [attributes.class, attribute.slice(1)].filter(Boolean).join(" ")
        break
      case attribute.startsWith("#"):
        attributes.id = attribute.slice(1)
        break
      default: {
        const [key, ...value] = attribute.split("=")
        attributes[key] = value.join("=").replace(/^"([\s\S]*)"$/, "$1")
      }
    }
  }
  return attributes
}