@peaceroad/markdown-it-footnote-here 0.3.0 → 0.3.1

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/index.js +95 -81
  2. package/package.json +6 -1
  3. package/AGENTS.md +0 -33
package/index.js CHANGED
@@ -1,10 +1,6 @@
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 = ''
5
- if (typeof env.docId === 'string') {
6
- prefix = '-' + env.docId + '-'
7
- }
1
+ const render_footnote_anchor_name = (tokens, idx, _opt, env) => {
2
+ const n = tokens[idx].meta.id + 1
3
+ const prefix = typeof env.docId === 'string' ? `-${env.docId}-` : ''
8
4
  return prefix + n
9
5
  }
10
6
 
@@ -22,28 +18,29 @@ const ensureNotesEnv = (env, key) => {
22
18
 
23
19
  const ENDNOTE_DOM_PREFIX = 'en'
24
20
 
25
- const getDomPrefix = (isEndnote) => (isEndnote ? ENDNOTE_DOM_PREFIX : 'fn')
26
- const getDisplayPrefix = (isEndnote, opt) => (isEndnote ? opt.endnotesLabelPrefix : '')
27
-
28
- const getNotesMeta = (token, env) => {
29
- if (token.meta && token.meta.isEndnote) return env.endnotes
30
- return env.footnotes
31
- }
32
-
33
21
  const selectNoteEnv = (label, env, preferEndnote) => {
34
- const endRefs = env.endnotes && env.endnotes.refs
35
22
  const footRefs = env.footnotes && env.footnotes.refs
36
- const endId = endRefs && endRefs[':' + label]
37
- const footId = footRefs && footRefs[':' + label]
23
+ const endRefs = env.endnotes && env.endnotes.refs
24
+ if (!footRefs && !endRefs) return null
25
+ const key = ':' + label
38
26
 
39
- if (preferEndnote && endId !== undefined) {
40
- return { env: env.endnotes, id: endId, isEndnote: true }
27
+ if (preferEndnote && endRefs) {
28
+ const endId = endRefs[key]
29
+ if (endId !== undefined) {
30
+ return { env: env.endnotes, id: endId, isEndnote: true }
31
+ }
41
32
  }
42
- if (footId !== undefined) {
43
- return { env: env.footnotes, id: footId, isEndnote: false }
33
+ if (footRefs) {
34
+ const footId = footRefs[key]
35
+ if (footId !== undefined) {
36
+ return { env: env.footnotes, id: footId, isEndnote: false }
37
+ }
44
38
  }
45
- if (endId !== undefined) {
46
- return { env: env.endnotes, id: endId, isEndnote: true }
39
+ if (!preferEndnote && endRefs) {
40
+ const endId = endRefs[key]
41
+ if (endId !== undefined) {
42
+ return { env: env.endnotes, id: endId, isEndnote: true }
43
+ }
47
44
  }
48
45
  return null
49
46
  }
@@ -52,18 +49,19 @@ const render_footnote_ref = (tokens, idx, opt, env) => {
52
49
  const token = tokens[idx]
53
50
  const id = token.meta.id
54
51
  const n = id + 1
55
- const notes = getNotesMeta(token, env)
56
- const isEndnote = !!token.meta.isEndnote
57
- const noteDomPrefix = getDomPrefix(isEndnote)
58
- const displayPrefix = getDisplayPrefix(isEndnote, opt)
59
- notes._refCount = notes._refCount || {}
60
- let refIdx = (notes._refCount[id] = (notes._refCount[id] || 0) + 1)
61
- if (!opt.afterBacklinkSuffixArabicNumerals) {
62
- refIdx = String.fromCharCode(96 + refIdx)
63
- }
52
+ const isEndnote = token.meta.isEndnote
53
+ const notes = isEndnote ? env.endnotes : env.footnotes
54
+ const noteDomPrefix = isEndnote ? ENDNOTE_DOM_PREFIX : 'fn'
55
+ const displayPrefix = isEndnote ? opt.endnotesLabelPrefix : ''
56
+ const totalCounts = notes.totalCounts ? notes.totalCounts[id] || 0 : 0
64
57
  let suffix = ''
65
58
  let label = `${opt.labelBra}${displayPrefix}${n}${opt.labelKet}`
66
- if (notes.totalCounts && notes.totalCounts[id] > 1) {
59
+ if (totalCounts > 1) {
60
+ const refCount = notes._refCount || (notes._refCount = [])
61
+ let refIdx = (refCount[id] = (refCount[id] || 0) + 1)
62
+ if (!opt.afterBacklinkSuffixArabicNumerals) {
63
+ refIdx = String.fromCharCode(96 + refIdx)
64
+ }
67
65
  suffix = '-' + refIdx
68
66
  if (opt.beforeSameBacklink) {
69
67
  label = `${opt.labelBra}${displayPrefix}${n}${suffix}${opt.labelKet}`
@@ -77,13 +75,13 @@ const render_footnote_ref = (tokens, idx, opt, env) => {
77
75
 
78
76
  const render_footnote_open = (tokens, idx, opt, env, slf) => {
79
77
  const id = slf.rules.footnote_anchor_name(tokens, idx, opt, env, slf)
80
- const isEndnote = tokens[idx].meta && tokens[idx].meta.isEndnote
78
+ const isEndnote = tokens[idx].meta.isEndnote
81
79
  if (isEndnote) return `<li id="${ENDNOTE_DOM_PREFIX}${id}">\n`
82
80
  return `<aside id="fn${id}" class="fn" role="doc-footnote">\n`
83
81
  }
84
82
 
85
83
  const render_footnote_close = (tokens, idx) => {
86
- const isEndnote = tokens[idx].meta && tokens[idx].meta.isEndnote
84
+ const isEndnote = tokens[idx].meta.isEndnote
87
85
  if (isEndnote) return `</li>\n`
88
86
  return `</aside>\n`
89
87
  }
@@ -91,15 +89,16 @@ const render_footnote_close = (tokens, idx) => {
91
89
  const render_footnote_anchor = (tokens, idx, opt, env) => {
92
90
  const idNum = tokens[idx].meta.id
93
91
  const n = idNum + 1
94
- const isEndnote = tokens[idx].meta && tokens[idx].meta.isEndnote
95
- const notes = getNotesMeta(tokens[idx], env)
96
- const counts = notes && notes.totalCounts
97
- const noteDomPrefix = getDomPrefix(!!isEndnote)
98
- const displayPrefix = getDisplayPrefix(!!isEndnote, opt)
92
+ const isEndnote = tokens[idx].meta.isEndnote
93
+ const notes = isEndnote ? env.endnotes : env.footnotes
94
+ const totalCounts = notes.totalCounts
95
+ const count = totalCounts ? totalCounts[idNum] || 0 : 0
96
+ const noteDomPrefix = isEndnote ? ENDNOTE_DOM_PREFIX : 'fn'
97
+ const displayPrefix = isEndnote ? opt.endnotesLabelPrefix : ''
99
98
 
100
- if (opt.beforeSameBacklink && counts && counts[idNum] > 1) {
99
+ if (opt.beforeSameBacklink && count > 1) {
101
100
  let links = ''
102
- for (let i = 1; i <= counts[idNum]; i++) {
101
+ for (let i = 1; i <= count; i++) {
103
102
  const suffix = '-' + String.fromCharCode(96 + i); // a, b, c ...
104
103
  links += `<a href="#${noteDomPrefix}-ref${n}${suffix}" class="${noteDomPrefix}-backlink" role="doc-backlink">${opt.backLabelBra}${displayPrefix}${n}${suffix}${opt.backLabelKet}</a>`
105
104
  }
@@ -110,7 +109,7 @@ const render_footnote_anchor = (tokens, idx, opt, env) => {
110
109
  return `<span class="${noteDomPrefix}-label">${opt.backLabelBra}${displayPrefix}${n}${opt.backLabelKet}</span> `
111
110
  }
112
111
 
113
- if (counts && counts[idNum] > 1) {
112
+ if (count > 1) {
114
113
  return `<a href="#${noteDomPrefix}-ref${n}-a" class="${noteDomPrefix}-backlink" role="doc-backlink">${opt.backLabelBra}${displayPrefix}${n}${opt.backLabelKet}</a> `
115
114
  }
116
115
 
@@ -199,7 +198,7 @@ const footnote_plugin = (md, option) =>{
199
198
  fn.refs[':' + label] = id
200
199
 
201
200
  const token = new state.Token('footnote_open', '', 1)
202
- token.meta = { id, label, isEndnote }
201
+ token.meta = { id, isEndnote }
203
202
  token.level = state.level++
204
203
  state.tokens.push(token)
205
204
  fn.positions.push(state.tokens.length - 1)
@@ -266,14 +265,13 @@ const footnote_plugin = (md, option) =>{
266
265
  }
267
266
 
268
267
  let pos = start + 2
269
- let found = false
270
- for (; pos < posMax && !found; pos++) {
268
+ for (; pos < posMax; pos++) {
271
269
  const ch = src.charCodeAt(pos)
270
+ if (ch === 0x5D /* ] */) break
272
271
  if (ch === 0x20 || ch === 0x0A) { return false; } // space or linebreak
273
- if (ch === 0x5D /* ] */) { found = true; break; }
274
272
  }
275
273
 
276
- if (!found || pos === start + 2) { return false; }
274
+ if (pos >= posMax || pos === start + 2) { return false; }
277
275
  pos++; // pos set next ']' position.
278
276
 
279
277
  const label = src.slice(start + 2, pos - 1)
@@ -285,16 +283,11 @@ const footnote_plugin = (md, option) =>{
285
283
  if (!silent) {
286
284
  const fn = resolved.env
287
285
 
288
- if (!fn.list) { fn.list = []; }
289
-
290
- const footnoteId = fn.list.length
291
- fn.list[footnoteId] = { label, count: 0 }
292
-
293
- fn.totalCounts = fn.totalCounts || {}
286
+ fn.totalCounts = fn.totalCounts || []
294
287
  fn.totalCounts[resolved.id] = (fn.totalCounts[resolved.id] || 0) + 1
295
288
 
296
289
  const token = state.push('footnote_ref', '', 0)
297
- token.meta = { id: resolved.id, label, isEndnote: resolved.isEndnote }
290
+ token.meta = { id: resolved.id, isEndnote: resolved.isEndnote }
298
291
  }
299
292
 
300
293
  state.pos = pos
@@ -306,7 +299,7 @@ const footnote_plugin = (md, option) =>{
306
299
  const tokens = state.tokens
307
300
  const createAnchorToken = (id, isEndnote) => {
308
301
  const aToken = new state.Token('footnote_anchor', '', 0)
309
- aToken.meta = { id, label: id + 1, isEndnote }
302
+ aToken.meta = { id, isEndnote }
310
303
  return aToken
311
304
  }
312
305
 
@@ -314,6 +307,30 @@ const footnote_plugin = (md, option) =>{
314
307
  const positions = notes && notes.positions
315
308
  if (!positions || positions.length === 0) { return; }
316
309
 
310
+ if (opt.afterBacklink) {
311
+ const noteDomPrefix = isEndnote ? ENDNOTE_DOM_PREFIX : 'fn'
312
+ const totalCounts = notes.totalCounts
313
+ for (let j = 0, len = positions.length; j < len; ++j) {
314
+ const posOpen = positions[j]
315
+ if (posOpen + 2 >= tokens.length) continue
316
+
317
+ const t1 = tokens[posOpen + 1]
318
+ if (t1.type !== 'paragraph_open') continue
319
+
320
+ const t2 = tokens[posOpen + 2]
321
+ if (t2.type !== 'inline') continue
322
+
323
+ const t0 = tokens[posOpen]
324
+ const id = t0.meta.id
325
+
326
+ t2.children.unshift(createAnchorToken(id, isEndnote))
327
+ const n = id + 1
328
+ const counts = totalCounts && totalCounts[id]
329
+ t2.children.push(createAfterBackLinkToken(state, counts, n, opt, noteDomPrefix, isEndnote))
330
+ }
331
+ return
332
+ }
333
+
317
334
  for (let j = 0, len = positions.length; j < len; ++j) {
318
335
  const posOpen = positions[j]
319
336
  if (posOpen + 2 >= tokens.length) continue
@@ -326,14 +343,8 @@ const footnote_plugin = (md, option) =>{
326
343
 
327
344
  const t0 = tokens[posOpen]
328
345
  const id = t0.meta.id
329
- const noteDomPrefix = isEndnote ? ENDNOTE_DOM_PREFIX : 'fn'
330
346
 
331
347
  t2.children.unshift(createAnchorToken(id, isEndnote))
332
- if (opt.afterBacklink) {
333
- const n = id + 1
334
- const counts = notes.totalCounts && notes.totalCounts[id]
335
- t2.children.push(createAfterBackLinkToken(state, counts, n, opt, noteDomPrefix, isEndnote))
336
- }
337
348
  }
338
349
  }
339
350
 
@@ -348,27 +359,31 @@ const footnote_plugin = (md, option) =>{
348
359
  }
349
360
 
350
361
  const tokens = state.tokens
351
- const endnoteBlocks = []
362
+ const endnoteTokens = []
352
363
 
353
- for (let i = 0; i < tokens.length; i++) {
364
+ let write = 0
365
+ let i = 0
366
+ while (i < tokens.length) {
354
367
  const token = tokens[i]
355
- if (token.type !== 'footnote_open' || !token.meta || !token.meta.isEndnote) continue
356
-
357
- let j = i + 1
358
- while (j < tokens.length) {
359
- const t = tokens[j]
360
- if (t.type === 'footnote_close' && t.meta && t.meta.isEndnote) {
361
- j++
362
- break
368
+ if (token.type === 'footnote_open' && token.meta && token.meta.isEndnote) {
369
+ endnoteTokens.push(token)
370
+ i++
371
+ while (i < tokens.length) {
372
+ const t = tokens[i]
373
+ endnoteTokens.push(t)
374
+ if (t.type === 'footnote_close' && t.meta && t.meta.isEndnote) {
375
+ i++
376
+ break
377
+ }
378
+ i++
363
379
  }
364
- j++
380
+ continue
365
381
  }
366
- endnoteBlocks.push(tokens.slice(i, j))
367
- tokens.splice(i, j - i)
368
- i--
382
+ tokens[write++] = token
383
+ i++
369
384
  }
370
385
 
371
- if (endnoteBlocks.length === 0) return
386
+ if (endnoteTokens.length === 0) return
372
387
 
373
388
  const sectionOpen = new state.Token('html_block', '', 0)
374
389
  const attrs = []
@@ -388,10 +403,9 @@ const footnote_plugin = (md, option) =>{
388
403
  const sectionClose = new state.Token('html_block', '', 0)
389
404
  sectionClose.content = '</ol>\n</section>\n'
390
405
 
406
+ tokens.length = write
391
407
  tokens.push(sectionOpen)
392
- endnoteBlocks.forEach(block => {
393
- tokens.push(...block)
394
- })
408
+ for (let j = 0; j < endnoteTokens.length; j++) tokens.push(endnoteTokens[j])
395
409
  tokens.push(sectionClose)
396
410
  }
397
411
 
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-footnote-here",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "A markdown-it plugin. This generate aside[role|doc-footnote] element just below the footnote reference paragraph.",
5
5
  "main": "index.js",
6
6
  "type":"module",
7
+ "files" : [
8
+ "index.js",
9
+ "LICENSE",
10
+ "README.md"
11
+ ],
7
12
  "author": "peaceroad <peaceroad@gmail.com>",
8
13
  "homepage": "https://github.com/peaceroad/markdown-it-footnote-here#readme",
9
14
  "repository": {
package/AGENTS.md DELETED
@@ -1,33 +0,0 @@
1
- # Workflow for Updating markdown-it-footnote-here
2
-
3
- This document captures the current implementation workflow, especially around footnotes/endnotes.
4
-
5
- ## Code overview
6
- - `index.js`:
7
- - Rendering helpers: `render_footnote_ref`, `render_footnote_open/close`, `render_footnote_anchor`, and `createAfterBackLinkToken`.
8
- - Parsing: custom block rule `footnote_def`, inline rule `footnote_ref`, core rule `footnote_anchor`, and `endnotes_move` to append endnotes at the end.
9
- - Endnote handling: labels starting with `endnotesPrefix` (default `en-`) are endnotes; DOM ids/classes use fixed `en`.
10
- - Options include backlink placement, label customization, and endnotes section attributes.
11
-
12
- ## Adding features / making changes
13
- 1) Review options and defaults in `index.js` (`opt` object).
14
- 2) Keep DOM prefixes stable:
15
- - Footnotes use `fn`; endnotes use `en` for ids/classes (`en1`, `en-ref1`, etc.).
16
- 3) Parsing flow:
17
- - `footnote_def` registers notes into `env.footnotes` or `env.endnotes` based on `endnotesPrefix`.
18
- - `footnote_ref` resolves references via `selectNoteEnv` and tags tokens with `isEndnote`.
19
- 4) Rendering flow:
20
- - `footnote_anchor` injects backlinks/labels into footnote content.
21
- - `endnotes_move` removes endnote blocks from inline positions and appends a `<section>` (attributes ordered aria-label → id → class → role).
22
- 5) When adding options:
23
- - Update `README.md` and add fixtures under `test/`.
24
- - Extend `test/test.js` to load the new fixtures.
25
-
26
- ## Testing
27
- - Run `npm test` (uses `test/test.js` and fixtures under `test/`).
28
- - Fixtures format: alternating `[Markdown]` and `[HTML]` blocks.
29
-
30
- ## Notes
31
- - `labelSupTag` applies to both footnotes and endnotes.
32
- - If `endnotesPrefix` is empty, endnotes are disabled.
33
- - `endnotesUseHeading` true: render `<h2>` with `endnotesSectionAriaLabel`; false: use `aria-label` without heading.