micromark / micromark-extension-frontmatter

Compare 9850845 ... +7 ... 74dd621

Coverage Reach

No flags found

Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.

e.g., #unittest #integration

#production #enterprise

#frontend #backend

Learn more about Codecov Flags here.

Showing 4 of 17 files from the diff.
Newly tracked file
dev/index.js created.
Newly tracked file
dev/lib/html.js created.
Newly tracked file
dev/lib/syntax.js created.
Newly tracked file
dev/lib/matters.js created.
Other files ignored by Codecov
package.json has changed.
lib/syntax.js was deleted.
.gitignore has changed.
test/index.js has changed.
.prettierignore has changed.
html.js was deleted.
lib/matters.js was deleted.
lib/html.js was deleted.
readme.md has changed.
index.js was deleted.

@@ -0,0 +1,5 @@
Loading
1 +
/**
2 +
 * @typedef {import('./lib/matters.js').Options} Options
3 +
 */
4 +
export {frontmatterHtml} from './lib/html.js'
5 +
export {frontmatter} from './lib/syntax.js'

@@ -0,0 +1,41 @@
Loading
1 +
/**
2 +
 * @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension
3 +
 * @typedef {import('micromark-util-types').Handle} Handle
4 +
 * @typedef {import('./matters.js').Options} Options
5 +
 */
6 +
7 +
import {matters} from './matters.js'
8 +
9 +
/**
10 +
 * Create an extension to support frontmatter (YAML, TOML, and more).
11 +
 *
12 +
 * @param {Options} [options='yaml'] One preset or matter, or an array of them.
13 +
 * @returns {HtmlExtension}
14 +
 */
15 +
export function frontmatterHtml(options) {
16 +
  const settings = matters(options)
17 +
  /** @type {HtmlExtension['enter']} */
18 +
  const enter = {}
19 +
  /** @type {HtmlExtension['exit']} */
20 +
  const exit = {}
21 +
  let index = -1
22 +
23 +
  while (++index < settings.length) {
24 +
    const type = settings[index].type
25 +
    enter[type] = start
26 +
    exit[type] = end
27 +
  }
28 +
29 +
  return {enter, exit}
30 +
31 +
  /** @type {Handle} */
32 +
  function start() {
33 +
    this.buffer()
34 +
  }
35 +
36 +
  /** @type {Handle} */
37 +
  function end() {
38 +
    this.resume()
39 +
    this.setData('slurpOneLineEnding', true)
40 +
  }
41 +
}

@@ -0,0 +1,210 @@
Loading
1 +
/**
2 +
 * @typedef {import('micromark-util-types').Extension} Extension
3 +
 * @typedef {import('micromark-util-types').ConstructRecord} ConstructRecord
4 +
 * @typedef {import('micromark-util-types').Construct} Construct
5 +
 * @typedef {import('micromark-util-types').Tokenizer} Tokenizer
6 +
 * @typedef {import('micromark-util-types').State} State
7 +
 * @typedef {import('./matters.js').Options} Options
8 +
 * @typedef {import('./matters.js').Matter} Matter
9 +
 * @typedef {import('./matters.js').Info} Info
10 +
 */
11 +
12 +
import {markdownLineEnding, markdownSpace} from 'micromark-util-character'
13 +
import {codes} from 'micromark-util-symbol/codes.js'
14 +
import {types} from 'micromark-util-symbol/types.js'
15 +
import {matters} from './matters.js'
16 +
17 +
/**
18 +
 * Create an extension to support frontmatter (YAML, TOML, and more).
19 +
 *
20 +
 * @param {Options} [options='yaml'] One preset or matter, or an array of them.
21 +
 * @returns {Extension}
22 +
 */
23 +
export function frontmatter(options) {
24 +
  const settings = matters(options)
25 +
  /** @type {ConstructRecord} */
26 +
  const flow = {}
27 +
  let index = -1
28 +
  /** @type {Matter} */
29 +
  let matter
30 +
  /** @type {number} */
31 +
  let code
32 +
33 +
  while (++index < settings.length) {
34 +
    matter = settings[index]
35 +
    code = fence(matter, 'open').charCodeAt(0)
36 +
    if (code in flow) {
37 +
      // @ts-expect-error it clearly does exist.
38 +
      flow[code].push(parse(matter))
39 +
    } else {
40 +
      flow[code] = [parse(matter)]
41 +
    }
42 +
  }
43 +
44 +
  return {flow}
45 +
}
46 +
47 +
/**
48 +
 * @param {Matter} matter
49 +
 * @returns {Construct}
50 +
 */
51 +
function parse(matter) {
52 +
  const name = matter.type
53 +
  const anywhere = matter.anywhere
54 +
  const valueType = name + 'Value'
55 +
  const fenceType = name + 'Fence'
56 +
  const sequenceType = fenceType + 'Sequence'
57 +
  const fenceConstruct = {tokenize: tokenizeFence, partial: true}
58 +
  /** @type {string} */
59 +
  let buffer
60 +
61 +
  return {tokenize: tokenizeFrontmatter, concrete: true}
62 +
63 +
  /** @type {Tokenizer} */
64 +
  function tokenizeFrontmatter(effects, ok, nok) {
65 +
    const self = this
66 +
67 +
    return start
68 +
69 +
    /** @type {State} */
70 +
    function start(code) {
71 +
      const position = self.now()
72 +
73 +
      if (position.column !== 1 || (!anywhere && position.line !== 1)) {
74 +
        return nok(code)
75 +
      }
76 +
77 +
      effects.enter(name)
78 +
      buffer = fence(matter, 'open')
79 +
      return effects.attempt(fenceConstruct, afterOpeningFence, nok)(code)
80 +
    }
81 +
82 +
    /** @type {State} */
83 +
    function afterOpeningFence(code) {
84 +
      buffer = fence(matter, 'close')
85 +
      return lineEnd(code)
86 +
    }
87 +
88 +
    /** @type {State} */
89 +
    function lineStart(code) {
90 +
      if (code === codes.eof || markdownLineEnding(code)) {
91 +
        return lineEnd(code)
92 +
      }
93 +
94 +
      effects.enter(valueType)
95 +
      return lineData(code)
96 +
    }
97 +
98 +
    /** @type {State} */
99 +
    function lineData(code) {
100 +
      if (code === codes.eof || markdownLineEnding(code)) {
101 +
        effects.exit(valueType)
102 +
        return lineEnd(code)
103 +
      }
104 +
105 +
      effects.consume(code)
106 +
      return lineData
107 +
    }
108 +
109 +
    /** @type {State} */
110 +
    function lineEnd(code) {
111 +
      // Require a closing fence.
112 +
      if (code === codes.eof) {
113 +
        return nok(code)
114 +
      }
115 +
116 +
      // Can only be an eol.
117 +
      effects.enter(types.lineEnding)
118 +
      effects.consume(code)
119 +
      effects.exit(types.lineEnding)
120 +
      return effects.attempt(fenceConstruct, after, lineStart)
121 +
    }
122 +
123 +
    /** @type {State} */
124 +
    function after(code) {
125 +
      effects.exit(name)
126 +
      return ok(code)
127 +
    }
128 +
  }
129 +
130 +
  /** @type {Tokenizer} */
131 +
  function tokenizeFence(effects, ok, nok) {
132 +
    let bufferIndex = 0
133 +
134 +
    return start
135 +
136 +
    /** @type {State} */
137 +
    function start(code) {
138 +
      if (code === buffer.charCodeAt(bufferIndex)) {
139 +
        effects.enter(fenceType)
140 +
        effects.enter(sequenceType)
141 +
        return insideSequence(code)
142 +
      }
143 +
144 +
      return nok(code)
145 +
    }
146 +
147 +
    /** @type {State} */
148 +
    function insideSequence(code) {
149 +
      if (bufferIndex === buffer.length) {
150 +
        effects.exit(sequenceType)
151 +
152 +
        if (markdownSpace(code)) {
153 +
          effects.enter(types.whitespace)
154 +
          return insideWhitespace(code)
155 +
        }
156 +
157 +
        return fenceEnd(code)
158 +
      }
159 +
160 +
      if (code === buffer.charCodeAt(bufferIndex++)) {
161 +
        effects.consume(code)
162 +
        return insideSequence
163 +
      }
164 +
165 +
      return nok(code)
166 +
    }
167 +
168 +
    /** @type {State} */
169 +
    function insideWhitespace(code) {
170 +
      if (markdownSpace(code)) {
171 +
        effects.consume(code)
172 +
        return insideWhitespace
173 +
      }
174 +
175 +
      effects.exit(types.whitespace)
176 +
      return fenceEnd(code)
177 +
    }
178 +
179 +
    /** @type {State} */
180 +
    function fenceEnd(code) {
181 +
      if (code === codes.eof || markdownLineEnding(code)) {
182 +
        effects.exit(fenceType)
183 +
        return ok(code)
184 +
      }
185 +
186 +
      return nok(code)
187 +
    }
188 +
  }
189 +
}
190 +
191 +
/**
192 +
 * @param {Matter} matter
193 +
 * @param {'open'|'close'} prop
194 +
 * @returns {string}
195 +
 */
196 +
function fence(matter, prop) {
197 +
  return matter.marker
198 +
    ? pick(matter.marker, prop).repeat(3)
199 +
    : // @ts-expect-error: They’re mutually exclusive.
200 +
      pick(matter.fence, prop)
201 +
}
202 +
203 +
/**
204 +
 * @param {Info|string} schema
205 +
 * @param {'open'|'close'} prop
206 +
 * @returns {string}
207 +
 */
208 +
function pick(schema, prop) {
209 +
  return typeof schema === 'string' ? schema : schema[prop]
210 +
}

@@ -0,0 +1,92 @@
Loading
1 +
/**
2 +
 * @typedef {'yaml'|'toml'} Preset
3 +
 *   Either `'yaml'` or `'toml'`
4 +
 *
5 +
 * @typedef Info
6 +
 * @property {string} open
7 +
 * @property {string} close
8 +
 *
9 +
 * @typedef MatterProps
10 +
 * @property {string} type
11 +
 *   Type to tokenize as
12 +
 * @property {boolean} [anywhere=false]
13 +
 *   If `true`, matter can be found anywhere in the document.
14 +
 *   If `false` (default), only matter at the start of the document is
15 +
 *   recognized.
16 +
 *
17 +
 * @typedef MarkerProps
18 +
 * @property {string|Info} marker
19 +
 *   Character used to construct fences.
20 +
 *   By providing an object with `open` and `close` different characters can be
21 +
 *   used for opening and closing fences.
22 +
 *   For example the character `'-'` will result in `'---'` being used as the
23 +
 *   fence
24 +
 * @property {never} [fence]
25 +
 *
26 +
 * @typedef FenceProps
27 +
 * @property {string|Info} fence
28 +
 *   String used as the complete fence.
29 +
 *   By providing an object with `open` and `close` different values can be used
30 +
 *   for opening and closing fences.
31 +
 *   This can be used too if fences contain different characters or lengths
32 +
 *   other than 3.
33 +
 * @property {never} [marker]
34 +
 *
35 +
 * @typedef {(MatterProps & FenceProps)|(MatterProps & MarkerProps)} Matter
36 +
 *
37 +
 * @typedef {Preset|Matter|Array.<Preset|Matter>} Options
38 +
 */
39 +
40 +
import {fault} from 'fault'
41 +
42 +
const own = {}.hasOwnProperty
43 +
const markers = {yaml: '-', toml: '+'}
44 +
45 +
/**
46 +
 * @param {Options} [options='yaml']
47 +
 * @returns {Array<Matter>}
48 +
 */
49 +
export function matters(options = 'yaml') {
50 +
  /** @type {Array<Matter>} */
51 +
  const results = []
52 +
  let index = -1
53 +
54 +
  // One preset or matter.
55 +
  if (!Array.isArray(options)) {
56 +
    options = [options]
57 +
  }
58 +
59 +
  while (++index < options.length) {
60 +
    results[index] = matter(options[index])
61 +
  }
62 +
63 +
  return results
64 +
}
65 +
66 +
/**
67 +
 * @param {Preset|Matter} option
68 +
 * @returns {Matter}
69 +
 */
70 +
function matter(option) {
71 +
  let result = option
72 +
73 +
  if (typeof result === 'string') {
74 +
    if (!own.call(markers, result)) {
75 +
      throw fault('Missing matter definition for `%s`', result)
76 +
    }
77 +
78 +
    result = {type: result, marker: markers[result]}
79 +
  } else if (typeof result !== 'object') {
80 +
    throw fault('Expected matter to be an object, not `%j`', result)
81 +
  }
82 +
83 +
  if (!own.call(result, 'type')) {
84 +
    throw fault('Missing `type` in matter `%j`', result)
85 +
  }
86 +
87 +
  if (!own.call(result, 'fence') && !own.call(result, 'marker')) {
88 +
    throw fault('Missing `marker` or `fence` in matter `%j`', result)
89 +
  }
90 +
91 +
  return result
92 +
}

Learn more Showing 4 files with coverage changes found.

New file dev/lib/matters.js
New
Loading file...
New file dev/lib/syntax.js
New
Loading file...
New file dev/index.js
New
Loading file...
New file dev/lib/html.js
New
Loading file...
Files Coverage
dev +100.00% 100.00%
Project Totals (4 files) 100.00%
Loading