@peaceroad/markdown-it-footnote-here 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +19 -6
  2. package/index.js +222 -182
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -26,21 +26,34 @@ HTML:
26
26
  <p>A paragraph.</p>
27
27
  ```
28
28
 
29
+ Notice. When multiple instances of the same footnote number appear in the main content, the default behavior is that the backlink from the footnote will refer to the first instance.
30
+
29
31
  ## Use
30
32
 
31
33
  ```js
32
- const md = require('markdown-it')()
33
- .use(require('@peaceroad/markdown-it-footnote-here'));
34
+ import mdit from 'markdown-it'
35
+ import mditFootnoteHere from '@peaceroad/markdown-it-footnote-here'
34
36
 
37
+ const md = mdit().use(mditFootnoteHere)
35
38
  md.render(/*...*/) // See examples above
36
39
  ```
37
40
 
38
41
  ## Install
39
42
 
40
- ```bash
43
+ ```samp
41
44
  npm install @peaceroad/markdown-it-footnote-here
42
45
  ```
43
46
 
44
- ## License
45
-
46
- [MIT](./LICENSE)
47
+ ## Options
48
+
49
+ - beforeSameBacklink (boolean): false by default. When true, duplicate footnote references will use letter suffixes (a, b, c, ...) and generate matching backlinks in footnote definitions.
50
+ - afterBacklink (boolean): false by default. If true, backlinks (↩) are placed at the end of the footnote content instead of before it.
51
+ - afterBacklinkContent (string): The content for the backlink (default: '↩').
52
+ - afterBacklinkWithNumber (boolean): If true, backlink will show a number or letter suffix.
53
+ - afterBacklinkSuffixArabicNumerals (boolean): If true, backlink suffix uses numbers (1, 2, ...) instead of letters (a, b, ...).
54
+ - afterBacklinkdAriaLabelPrefix (string): Prefix for aria-label of backlink (default: 'Back to reference ').
55
+ - labelBra (string): Bracket to use before footnote number (default: '[').
56
+ - labelKet (string): Bracket to use after footnote number (default: ']').
57
+ - labelSupTag (boolean): If true, wraps footnote reference in `<sup>` tag.
58
+ - backLabelBra (string): Bracket to use before backlink number (default: '[').
59
+ - backLabelKet (string): Bracket to use after backlink number (default: ']').
package/index.js CHANGED
@@ -1,248 +1,288 @@
1
- // Process footnotes
2
- //
3
- 'use strict';
4
-
5
- ////////////////////////////////////////////////////////////////////////////////
6
- // Renderer partials
7
-
8
- function render_footnote_anchor_name(tokens, idx, options, env/*, slf*/) {
9
- var n = Number(tokens[idx].meta.id + 1).toString();
10
- var prefix = '';
11
-
1
+ const render_footnote_anchor_name = (tokens, idx, opt, env) => {
2
+ let n = tokens[idx].meta.id + 1
3
+ if (!opt.afterBacklinkSuffixArabicNumerals) n = Number(n).toString()
4
+ let prefix = ''
12
5
  if (typeof env.docId === 'string') {
13
- prefix = '-' + env.docId + '-';
6
+ prefix = '-' + env.docId + '-'
14
7
  }
15
-
16
- return prefix + n;
8
+ return prefix + n
17
9
  }
18
10
 
19
- function render_footnote_caption(tokens, idx) {
20
- var n = Number(tokens[idx].meta.id + 1).toString();
21
-
22
- return '[' + n + ']';
11
+ const render_footnote_ref = (tokens, idx, opt, env) => {
12
+ const token = tokens[idx]
13
+ const id = token.meta.id
14
+ const n = id + 1
15
+ const footnotes = env.footnotes
16
+ footnotes._refCount = footnotes._refCount || {}
17
+ let refIdx = (footnotes._refCount[id] = (footnotes._refCount[id] || 0) + 1)
18
+ if (!opt.afterBacklinkSuffixArabicNumerals) {
19
+ refIdx = String.fromCharCode(96 + refIdx)
20
+ }
21
+ let suffix = ''
22
+ let label = `${opt.labelBra}${n}${opt.labelKet}`
23
+ if (footnotes.totalCounts && footnotes.totalCounts[id] > 1) {
24
+ suffix = '-' + refIdx
25
+ if (opt.beforeSameBacklink) {
26
+ label = `${opt.labelBra}${n}${suffix}${opt.labelKet}`
27
+ }
28
+ }
29
+ let refCont = `<a href="#fn${n}" id="fn-ref${n}${suffix}" class="fn-noteref" role="doc-noteref">${label}</a>`
30
+ if (opt.labelSupTag) refCont = `<sup class="fn-noteref-wrapper">${refCont}</sup>`
31
+ return refCont
23
32
  }
24
33
 
25
- function render_footnote_ref(tokens, idx, options, env, slf) {
26
- var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
27
- var caption = slf.rules.footnote_caption(tokens, idx, options, env, slf);
28
- var refid = id;
34
+ const render_footnote_open = (tokens, idx, opt, env, slf) => {
35
+ const id = slf.rules.footnote_anchor_name(tokens, idx, opt, env, slf)
36
+ return `<aside id="fn${id}" class="fn" role="doc-footnote">\n`
37
+ }
29
38
 
30
- return '<a href="#fn' + id + '" id="fn-ref' + refid + '" class="fn-noteref" role="doc-noteref">' + caption + '</a>';
39
+ const render_footnote_close = () => {
40
+ return `</aside>\n`
31
41
  }
32
42
 
33
- function render_footnote_open(tokens, idx, options, env, slf) {
34
- var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
43
+ const render_footnote_anchor = (tokens, idx, opt, env) => {
44
+ const idNum = tokens[idx].meta.id
45
+ const n = idNum + 1
46
+ const footnotes = env.footnotes
47
+ const counts = footnotes && footnotes.totalCounts
48
+ if (opt.beforeSameBacklink && counts && counts[idNum] > 1) {
49
+ let links = ''
50
+ for (let i = 1; i <= counts[idNum]; i++) {
51
+ const suffix = '-' + String.fromCharCode(96 + i); // a, b, c ...
52
+ links += `<a href="#fn-ref${n}${suffix}" class="fn-backlink" role="doc-backlink">${opt.backLabelBra}${n}${suffix}${opt.backLabelKet}</a>`
53
+ }
54
+ return links + ' '
55
+ }
35
56
 
36
- return '<aside id="fn' + id + '" class="fn" role="doc-footnote">\n';
37
- }
57
+ if (opt.afterBacklink) {
58
+ return `<span class="fn-label">${opt.backLabelBra}${n}${opt.backLabelKet}</span> `
59
+ }
60
+
61
+ if (counts && counts[idNum] > 1) {
62
+ return `<a href="#fn-ref${n}-a" class="fn-backlink" role="doc-backlink">${opt.backLabelBra}${n}${opt.backLabelKet}</a> `
63
+ }
38
64
 
39
- function render_footnote_close() {
40
- return '</aside>\n';
65
+ return `<a href="#fn-ref${n}" class="fn-backlink" role="doc-backlink">${opt.backLabelBra}${n}${opt.backLabelKet}</a> `
41
66
  }
42
67
 
43
- function render_footnote_anchor(tokens, idx, options, env, slf) {
44
- var id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
45
- if (tokens[idx].meta.subId > 0) {
46
- id += ':' + tokens[idx].meta.subId;
68
+ function createAfterBackLinkToken(state, counts, n, opt) {
69
+ let html = ' '
70
+ if (counts && counts > 1) {
71
+ for (let i = 1; i <= counts; i++) {
72
+ const suffixChar = opt.afterBacklinkSuffixArabicNumerals ? i : String.fromCharCode(96 + i)
73
+ const suffix = '-' + suffixChar
74
+ html += `<a href="#fn-ref${n}${suffix}" class="fn-backlink" role="doc-backlink"`
75
+ if (opt.afterBacklinkdAriaLabelPrefix) html += ` aria-label="${opt.afterBacklinkdAriaLabelPrefix}${n}${suffix}"`
76
+ html += `>${opt.afterBacklinkContent}`
77
+ if (opt.afterBacklinkWithNumber) {
78
+ html += `<sup>${suffixChar}</sup>`
79
+ }
80
+ html += `</a>`
81
+ }
82
+ } else {
83
+ html += `<a href="#fn-ref${n}" class="fn-backlink" role="doc-backlink"`
84
+ if (opt.afterBacklinkdAriaLabelPrefix) html += ` aria-label="${opt.afterBacklinkdAriaLabelPrefix}${n}"`
85
+ html += `>${opt.afterBacklinkContent}</a>`
47
86
  }
48
- return '<a href="#fn-ref' + id + '" class="fn-backlink" role="doc-backlink">[' + id + ']</a> ';
87
+ const token = new state.Token('html_inline', '', 0)
88
+ token.content = html
89
+ return token
49
90
  }
50
91
 
92
+ const footnote_plugin = (md, option) =>{
93
+ const opt = {
94
+ labelBra: '[',
95
+ labelKet: ']',
96
+ labelSupTag: false,
97
+ backLabelBra: '[',
98
+ backLabelKet: ']',
99
+ beforeSameBacklink: false,
100
+ afterBacklink: false,
101
+ afterBacklinkContent: '↩',
102
+ afterBacklinkWithNumber: false,
103
+ afterBacklinkSuffixArabicNumerals: false,
104
+ afterBacklinkdAriaLabelPrefix: 'Back to reference ', /* 戻る:本文参照 */
105
+ }
106
+ if (option) Object.assign(opt, option)
51
107
 
52
- module.exports = function footnote_plugin(md) {
53
- const parseLinkLabel = md.helpers.parseLinkLabel;
54
- const isSpace = md.utils.isSpace;
55
-
56
- md.renderer.rules.footnote_ref = render_footnote_ref;
57
- md.renderer.rules.footnote_open = render_footnote_open;
58
- md.renderer.rules.footnote_close = render_footnote_close;
59
- md.renderer.rules.footnote_anchor = render_footnote_anchor;
108
+ const isSpace = md.utils.isSpace
60
109
 
61
- // helpers (only used in other rules, no tokens are attached to those)
62
- md.renderer.rules.footnote_caption = render_footnote_caption;
63
- md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
110
+ md.renderer.rules.footnote_ref = (tokens, idx, _options, env) => render_footnote_ref(tokens, idx, opt, env)
111
+ md.renderer.rules.footnote_open = render_footnote_open
112
+ md.renderer.rules.footnote_close = render_footnote_close
113
+ md.renderer.rules.footnote_anchor = (tokens, idx, _options, env, slf) => render_footnote_anchor(tokens, idx, opt, env, slf)
114
+ md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name
64
115
 
65
116
  // Process footnote block definition
66
- function footnote_def(state, startLine, endLine, silent) {
67
- var oldBMark, oldTShift, oldSCount, oldParentType, pos, label, token, id,
68
- initial, offset, ch, posAfterColon,
69
- start = state.bMarks[startLine] + state.tShift[startLine],
70
- max = state.eMarks[startLine];
117
+ const footnote_def = (state, startLine, endLine, silent) => {
118
+ const bMarks = state.bMarks, tShift = state.tShift, eMarks = state.eMarks, src = state.src
119
+ const start = bMarks[startLine] + tShift[startLine]
120
+ const max = eMarks[startLine]
71
121
 
72
122
  // line should be at least 5 chars - "[^x]:"
73
123
  if (start + 4 > max) { return false; }
74
124
 
75
- if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; }
76
- if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; }
77
-
78
- for (pos = start + 2; pos < max; pos++) {
79
- if (state.src.charCodeAt(pos) === 0x20) { return false; }
80
- if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
81
- break;
82
- }
83
- }
84
-
85
- if (pos === start + 2) { return false; } // no empty footnote labels
86
- if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) { return false; }
87
- if (silent) { return true; }
88
- pos++;
125
+ if (src.charCodeAt(start) !== 0x5B/* [ */ || src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; }
89
126
 
90
- if (!state.env.footnotes) { state.env.footnotes = { length:0}; }
91
- if (!state.env.footnotes.refs) { state.env.footnotes.refs = {}; }
127
+ // locate end of label efficiently
128
+ const idx = src.indexOf(']:', start + 2)
129
+ if (idx < start + 3 || idx > max - 2) { return false; }
92
130
 
93
- label = state.src.slice(start + 2, pos - 2);
94
- id = state.env.footnotes.length++;
95
- state.env.footnotes.refs[':' + label] = id;
131
+ const label = src.slice(start + 2, idx)
132
+ if (label.indexOf(' ') >= 0) { return false; }
133
+ const pos = idx + 2
96
134
 
97
- token = new state.Token('footnote_open', '', 1);
98
- token.meta = { id: id, label: label };
99
- token.level = state.level++;
100
- state.tokens.push(token);
101
-
102
- oldBMark = state.bMarks[startLine];
103
- oldTShift = state.tShift[startLine];
104
- oldSCount = state.sCount[startLine];
105
- oldParentType = state.parentType;
106
-
107
- posAfterColon = pos;
108
- initial = offset = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]);
109
-
110
- /*
111
- token = new state.Token('footnote_anchor', '', 0);
112
- token.meta = { id: id, label: label };
113
- state.tokens.push(token);
114
- */
115
-
116
-
117
- while (pos < max) {
118
- ch = state.src.charCodeAt(pos);
135
+ if (silent) { return true; }
119
136
 
137
+ // initialize footnotes environment once
138
+ if (!state.env.footnotes) {
139
+ state.env.footnotes = { length: 0, refs: {}, positions: [] }
140
+ }
141
+ const fn = state.env.footnotes
142
+ const id = fn.length++
143
+ fn.refs[':' + label] = id
144
+
145
+ const token = new state.Token('footnote_open', '', 1)
146
+ token.meta = { id, label }
147
+ token.level = state.level++
148
+ state.tokens.push(token)
149
+ fn.positions.push(state.tokens.length - 1)
150
+
151
+ const oldBMark = bMarks[startLine]
152
+ const oldTShift = tShift[startLine]
153
+ const oldSCount = state.sCount[startLine]
154
+ const oldParentType = state.parentType
155
+
156
+ const posAfterColon = pos
157
+ const initial = state.sCount[startLine] + pos - (bMarks[startLine] + tShift[startLine])
158
+ let offset = initial
159
+ let newPos = pos
160
+
161
+ while (newPos < max) {
162
+ const ch = src.charCodeAt(newPos)
120
163
  if (isSpace(ch)) {
121
164
  if (ch === 0x09) {
122
- offset += 4 - offset % 4;
165
+ offset += 4 - offset % 4
123
166
  } else {
124
- offset++;
167
+ offset++
125
168
  }
126
169
  } else {
127
- break;
170
+ break
128
171
  }
129
-
130
- pos++;
172
+ newPos++
131
173
  }
132
174
 
133
- state.tShift[startLine] = pos - posAfterColon;
134
- state.sCount[startLine] = offset - initial;
175
+ state.tShift[startLine] = newPos - posAfterColon
176
+ state.sCount[startLine] = offset - initial
135
177
 
136
- state.bMarks[startLine] = posAfterColon;
137
- state.blkIndent += 4;
138
- state.parentType = 'footnote';
178
+ state.bMarks[startLine] = posAfterColon
179
+ state.blkIndent += 4
180
+ state.parentType = 'footnote'
139
181
 
140
182
  if (state.sCount[startLine] < state.blkIndent) {
141
- state.sCount[startLine] += state.blkIndent;
183
+ state.sCount[startLine] += state.blkIndent
142
184
  }
143
185
 
144
- state.md.block.tokenize(state, startLine, endLine, true);
186
+ state.md.block.tokenize(state, startLine, endLine, true)
145
187
 
146
- state.parentType = oldParentType;
147
- state.blkIndent -= 4;
148
- state.tShift[startLine] = oldTShift;
149
- state.sCount[startLine] = oldSCount;
150
- state.bMarks[startLine] = oldBMark;
188
+ state.parentType = oldParentType
189
+ state.blkIndent -= 4
190
+ state.tShift[startLine] = oldTShift
191
+ state.sCount[startLine] = oldSCount
192
+ state.bMarks[startLine] = oldBMark
151
193
 
152
- token = new state.Token('footnote_close', '', -1);
153
- token.level = --state.level;
154
- state.tokens.push(token);
194
+ const closeToken = new state.Token('footnote_close', '', -1)
195
+ closeToken.level = --state.level
196
+ state.tokens.push(closeToken)
155
197
 
156
- return true;
198
+ return true
157
199
  }
158
200
 
159
201
  // Process footnote references ([^...])
160
- function footnote_ref(state, silent) {
161
- var label,
162
- pos,
163
- id,
164
- footnoteId,
165
- token,
166
- max = state.posMax,
167
- start = state.pos;
168
-
169
- // should be at least 4 chars - "[^x]"
170
- if (start + 3 > max) { return false; }
171
-
172
- if (!state.env.footnotes || !state.env.footnotes.refs) { return false; }
173
- if (state.src.charCodeAt(start) !== 0x5B/* [ */) { return false; }
174
- if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) { return false; }
175
-
176
- for (pos = start + 2; pos < max; pos++) {
177
- if (state.src.charCodeAt(pos) === 0x20) { return false; }
178
- if (state.src.charCodeAt(pos) === 0x0A) { return false; }
179
- if (state.src.charCodeAt(pos) === 0x5D /* ] */) {
180
- break;
181
- }
202
+ const footnote_ref = (state, silent) => {
203
+ const src = state.src
204
+ const start = state.pos
205
+ const posMax = state.posMax
206
+ if (start + 3 >= posMax) { return false; } // - "[^x]"
207
+ if (src.charCodeAt(start) !== 0x5B/* [ */ || src.charCodeAt(start + 1) !== 0x5E/* ^ */) {
208
+ return false
209
+ }
210
+
211
+ const env = state.env
212
+ if (!env.footnotes || !env.footnotes.refs) { return false; }
213
+
214
+ let pos = start + 2
215
+ let found = false
216
+ for (; pos < posMax && !found; pos++) {
217
+ const ch = src.charCodeAt(pos)
218
+ if (ch === 0x20 || ch === 0x0A) { return false; } // space or linebreak
219
+ if (ch === 0x5D /* ] */) { found = true; break; }
182
220
  }
183
221
 
184
- if (pos === start + 2) { return false; } // no empty footnote labels
185
- if (pos >= max) { return false; }
186
- pos++;
222
+ if (!found || pos === start + 2) { return false; }
223
+ pos++; // pos set next ']' position.
187
224
 
188
- label = state.src.slice(start + 2, pos - 1);
189
- if (typeof state.env.footnotes.refs[':' + label] === 'undefined') { return false; }
225
+ const label = src.slice(start + 2, pos - 1)
226
+ const id = env.footnotes.refs[':' + label]
227
+
228
+ if (id === undefined) { return false; }
190
229
 
191
230
  if (!silent) {
192
- if (!state.env.footnotes.list) { state.env.footnotes.list = []; }
231
+ const fn = env.footnotes
193
232
 
233
+ if (!fn.list) { fn.list = []; }
194
234
 
195
- let footnoteId = state.env.footnotes.list.length;
196
- state.env.footnotes.list[footnoteId] = { label: label, count: 0 };
235
+ const footnoteId = fn.list.length
236
+ fn.list[footnoteId] = { label, count: 0 }
197
237
 
198
- id = state.env.footnotes.refs[':' + label];
238
+ fn.totalCounts = fn.totalCounts || {}
239
+ fn.totalCounts[id] = (fn.totalCounts[id] || 0) + 1
199
240
 
200
- token = state.push('footnote_ref', '', 0);
201
- token.meta = { id: id, label: label };
241
+ const token = state.push('footnote_ref', '', 0)
242
+ token.meta = { id, label }
202
243
  }
203
244
 
204
- state.pos = pos;
205
- state.posMax = max;
206
- return true;
245
+ state.pos = pos
246
+ return true
207
247
  }
208
248
 
209
- function addClass(token, className) {
210
- token.attrs = token.attrs || [];
211
- const ats = token.attrs.map(x => x[0]);
212
- const i = ats.indexOf('class');
213
- if (i === -1) {
214
- token.attrs.push(['class', className]);
215
- } else {
216
- let classVal = token.attrs[i][1] || '';
217
- const classNames = classStr.split(' ');
218
- if (classNames.indexOf(className) === -1) {
219
- classVal += ' ' + className;
220
- token.attrs[i][1] = classVal;
221
- }
249
+ const footnote_anchor = (state) => {
250
+ const tokens = state.tokens
251
+ const fn = state.env.footnotes
252
+ const positions = fn && fn.positions
253
+ if (!positions || positions.length === 0) { return; }
254
+
255
+ const createAnchorToken = (id) => {
256
+ const aToken = new state.Token('footnote_anchor', '', 0)
257
+ aToken.meta = { id, label: id + 1 }
258
+ return aToken
222
259
  }
223
- }
224
260
 
225
- function footnote_anchor (state) {
226
- let n = 0;
227
- let id = 0;
228
- while (n < state.tokens.length - 2) {
229
- const token = state.tokens[n];
230
- const isFootnoteStartTag = token.type === 'footnote_open';
231
- if(!isFootnoteStartTag) {
232
- n++;
233
- continue;
234
- }
235
- if(state.tokens[n + 1].type === 'paragraph_open'
236
- && state.tokens[n + 2].type === 'inline') {
237
- const aToken = new state.Token('footnote_anchor', '', 0);
238
- aToken.meta = { id: id++, label: id };
239
- state.tokens[n + 2].children.unshift(aToken);
261
+ for (let j = 0, len = positions.length; j < len; ++j) {
262
+ const posOpen = positions[j]
263
+ if (posOpen + 2 >= tokens.length) continue
264
+
265
+ const t1 = tokens[posOpen + 1]
266
+ if (t1.type !== 'paragraph_open') continue
267
+
268
+ const t2 = tokens[posOpen + 2]
269
+ if (t2.type !== 'inline') continue
270
+
271
+ const t0 = tokens[posOpen]
272
+ const id = t0.meta.id
273
+
274
+ t2.children.unshift(createAnchorToken(id))
275
+ if (opt.afterBacklink) {
276
+ const n = id + 1
277
+ const counts = fn.totalCounts && fn.totalCounts[id]
278
+ t2.children.push(createAfterBackLinkToken(state, counts, n, opt))
240
279
  }
241
- n++;
242
280
  }
243
281
  }
244
282
 
245
- md.block.ruler.before('reference', 'footnote_def', footnote_def, { alt: [ 'paragraph', 'reference' ] });
246
- md.inline.ruler.after('image', 'footnote_ref', footnote_ref);
247
- md.core.ruler.after('inline', 'footnote_anchor', footnote_anchor);
248
- };
283
+ md.block.ruler.before('reference', 'footnote_def', footnote_def, { alt: [ 'paragraph', 'reference' ] })
284
+ md.inline.ruler.after('image', 'footnote_ref', footnote_ref)
285
+ md.core.ruler.after('inline', 'footnote_anchor', footnote_anchor)
286
+ }
287
+
288
+ export default footnote_plugin
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-footnote-here",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A markdown-it plugin. This generate aside[role|doc-footnote] element just below the footnote reference paragraph.",
5
+ "type":"module",
5
6
  "author": "peaceroad <peaceroad@gmail.com>",
6
7
  "repository": "https://github.com/peaceroad/markdown-it-footnote-here.git",
7
8
  "bugs": {
@@ -12,6 +13,6 @@
12
13
  "test": "node test/test.js"
13
14
  },
14
15
  "devDependencies": {
15
- "markdown-it": "^12.0.6"
16
+ "markdown-it": "^14.1.0"
16
17
  }
17
18
  }