All files / markdown / plugins / directives.ts

100.00% Branches 2/2
100.00% Lines 24/24
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
 
 
 
x2
x2
x2
 
 
 
 
 
 
x2
x2
x3
x3
x2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
x2
x3
x3
x4
x5
x6
x10
x13
x13
x11
x6
x5
x4
x4
x3
x3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 





















































































































































































































// Imports
import type { Plugin } from "../renderer.ts"
import type { Arg, Arrayable, Nullable, Optional } from "@libs/typing/types"
import remarkDirective from "remark-directive"
import { visit } from "unist-util-visit"
export { h } from "hastscript"

/**
 * Add support for custom directives.
 *
 * {@link https://github.com/remarkjs/remark-directive?tab=readme-ov-file#examples | See remark-directive for more information}.
 */
export default {
  remark(processor) {
    return processor.use(remarkDirective)
  },
} as Plugin

/**
 * Create a new custom directive.
 *
 * ```ts
 * import { Renderer } from "../renderer.ts"
 * import directives, { directive, h } from "./directives.ts"
 *
 * const foo = directive((node) => {
 *   node.data ??= {}
 *   node.data.hName = "div"
 *   node.data.hProperties = h(node.data.hName, { bar: "qux" }).properties
 * })
 *
 * const markdown = new Renderer({ plugins: [directives, foo] })
 * await markdown.render(`
 * :::foo
 * baz
 * :::
 * `.trim())
 * ```
 */
export function directive(callback: (node: AstNode) => void): Plugin {
  return {
    remark(processor) {
      return processor.use(function () {
        return function (tree: Arg<typeof visit>) {
          visit(tree, (node) => {
            if (!/(?:container|leaf|text)Directive/.test(node.type)) {
              return
            }
            callback(node as AstNode)
          })
        }
      })
    },
  } as Plugin
}

// =======================================================================================================
// The following definitions were adapted from:
// - https://github.com/DefinitelyTyped/DefinitelyTyped/blob/a6e9e491ff5d0fd7f438fb77a70b54b28e356ced/types/hast/v2/index.d.ts
// - https://github.com/DefinitelyTyped/DefinitelyTyped/blob/a6e9e491ff5d0fd7f438fb77a70b54b28e356ced/types/unist/v2/index.d.ts

/** Information associated by the ecosystem with the node. */
export interface AstData {
  /**
   * Signal that a node should result in something with these children.
   *
   * When this is defined, when a parent is created, these children will be used.
   */
  hChildren?: AstElementContent[]

  /**
   * Signal that a node should result in a particular element, instead of its default behavior.
   *
   * When this is defined, an element with the given tag name is created.
   * For example, when setting `hName` to `'b'`, a `<b>` element is created.
   */
  hName?: string

  /**
   * Signal that a node should result in an element with these properties.
   *
   * When this is defined, when an element is created, these properties will be used.
   */
  hProperties?: AstProperties

  /** Represents information associated with an element. */
  [key: string]: unknown
}

/** Represents information associated with an element. */
export interface AstProperties {
  /** Represents information associated with an element. */
  [PropertyName: string]: Optional<Nullable<boolean | Arrayable<string | number>>>
}

/** Syntactic units in unist syntax trees are called nodes. */
export interface AstNode<Data extends object = AstData, ChildNode = unknown> {
  /** The variant of a node. */
  type: string
  /** Information from the ecosystem. */
  data?: Data
  /** Location of a node in a source document. Must not be present if a node is generated. */
  position?: AstPosition
  /** Directive name. */
  name?: string
  /** Directive attributes. */
  attributes?: Nullable<Record<string, Optional<Nullable<string>>>>
  /** Children of the directive. */
  children: ChildNode[]
}

/** Util for extracting type of {@link Node.data}. */
export type AstNodeData<Node extends AstNode<object>> = Node extends AstNode<infer TData> ? TData : never

/** Location of a node in a source file. */
export interface AstPosition {
  /** Place of the first character of the parsed source region. */
  start: AstPoint
  /** Place of the first character after the parsed source region. */
  end: AstPoint
  /** Start column at each index (plus start line) in the source region, for elements that span multiple lines. */
  indent?: number[]
}

/** One place in a source file. */
export interface AstPoint {
  /** Line in a source file (1-indexed integer). */
  line: number
  /** Column in a source file (1-indexed integer). */
  column: number
  /** Character in a source file (0-indexed integer). */
  offset?: number
}

/** Node in hast containing other nodes. */
export interface AstParent<ChildNode extends AstNode<object> = AstContent, Data extends object = AstNodeData<ChildNode>> extends AstNode<Data> {
  /** List representing the children of a node. */
  children: ChildNode[]
}

/** Represents a root or element node. */
export type AstContent = AstRootContent | AstElementContent

/** Represents a root node content. */
export type AstRootContent = AstRootContentMap[keyof AstRootContentMap]

/** Represents an element node content. */
export type AstElementContent = AstElementContentMap[keyof AstElementContentMap]

/** Represents an HTML DocumentType. */
export interface AstDocType extends AstNode {
  /** Represents this variant of a Node. */
  type: "doctype"
  /** Represents the document name. */
  name: string
}

/**
 * Root represents a document.
 * Can be used as the root of a tree, or as a value of the content field on a 'template' Element, never as a child.
 */
export interface AstRoot extends AstParent {
  /** Represents this variant of a Node. */
  type: "root"
  /** List representing the children of a node. */
  children: AstRootContent[]
}

/** This map registers all node types that may be used as top-level content in the document. These types are accepted inside `root` nodes. */
export interface AstRootContentMap extends AstElementContentMap {
  /** Represents an HTML DocumentType. */
  doctype: AstDocType
}

/** Element represents an HTML Element. */
export interface AstElement extends AstParent {
  /** Represents this variant of a Node. */
  type: "element"
  /** Represents the element’s local name. */
  tagName: string
  /** Represents information associated with the element. */
  properties?: AstProperties
  /** If the tagName field is 'template', a content field can be present. */
  content?: AstRoot
  /** List representing the children of a node. */
  children: AstElementContent[]
}

/** This map registers all node types that may be used as content in an element. These types are accepted inside `element` nodes. */
export interface AstElementContentMap {
  /** Represents an HTML Element. */
  // deno-lint-ignore no-explicit-any
  element: any // AstElement
  /** Represents an HTML Comment. */
  comment: AstComment
  /** Represents an HTML Text. */
  text: AstText
}

/** Nodes in hast containing a value. */
export interface AstLiteral extends AstNode {
  /** Represents the value of a node. */
  value: string
}

/** Represents an HTML Comment. */
export interface AstComment extends AstLiteral {
  /** Represents this variant of a Literal. */
  type: "comment"
}

/** Represents an HTML Text. */
export interface AstText extends AstLiteral {
  /** Represents this variant of a Literal. */
  type: "text"
}