oast-to-hast 4.5.2 → 4.6.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.
@@ -1 +1 @@
1
- {"version":3,"file":"headline.d.ts","sourceRoot":"","sources":["headline.js"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,gCAJW,OAAO,aAAa,EAAE,KAAK,QAC3B,OAAO,MAAM,EAAE,QAAQ,GACrB,OAAO,MAAM,EAAE,OAAO,CASlC"}
1
+ {"version":3,"file":"headline.d.ts","sourceRoot":"","sources":["headline.js"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,gCAJW,OAAO,aAAa,EAAE,KAAK,QAC3B,OAAO,MAAM,EAAE,QAAQ,GACrB,OAAO,MAAM,EAAE,OAAO,CAUlC"}
@@ -4,10 +4,11 @@
4
4
  * @returns {import('hast').Element}
5
5
  */
6
6
  export function headline(state, node) {
7
+ const id = state.ids.byNode.get(node)
7
8
  return state.patch(node, {
8
9
  type: 'element',
9
10
  tagName: `h${node.level}`,
10
- properties: {},
11
+ properties: id ? { id } : {},
11
12
  children: state.all(node)
12
13
  })
13
14
  }
@@ -4,4 +4,10 @@
4
4
  * @returns {import('hast').Element|undefined}
5
5
  */
6
6
  export function section(state: import("../state.js").State, node: import("orga").Section): import("hast").Element | undefined;
7
+ /**
8
+ * @param {Pick<import('../state.js').Config, 'selectTags' | 'excludeTags'>} config
9
+ * @param {string[]} tags
10
+ * @returns {boolean}
11
+ */
12
+ export function shouldSkip({ selectTags, excludeTags }: Pick<import("../state.js").Config, "selectTags" | "excludeTags">, tags: string[]): boolean;
7
13
  //# sourceMappingURL=section.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"section.d.ts","sourceRoot":"","sources":["section.js"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,+BAJW,OAAO,aAAa,EAAE,KAAK,QAC3B,OAAO,MAAM,EAAE,OAAO,GACpB,OAAO,MAAM,EAAE,OAAO,GAAC,SAAS,CA6B5C"}
1
+ {"version":3,"file":"section.d.ts","sourceRoot":"","sources":["section.js"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,+BAJW,OAAO,aAAa,EAAE,KAAK,QAC3B,OAAO,MAAM,EAAE,OAAO,GACpB,OAAO,MAAM,EAAE,OAAO,GAAC,SAAS,CA6B5C;AAED;;;;GAIG;AACH,wDAJW,IAAI,CAAC,OAAO,aAAa,EAAE,MAAM,EAAE,YAAY,GAAG,aAAa,CAAC,QAChE,MAAM,EAAE,GACN,OAAO,CAYnB"}
@@ -33,11 +33,11 @@ export function section(state, node) {
33
33
  }
34
34
 
35
35
  /**
36
- * @param {import('../state.js').Config} config
36
+ * @param {Pick<import('../state.js').Config, 'selectTags' | 'excludeTags'>} config
37
37
  * @param {string[]} tags
38
38
  * @returns {boolean}
39
39
  */
40
- function shouldSkip({ selectTags = [], excludeTags = [] }, tags) {
40
+ export function shouldSkip({ selectTags = [], excludeTags = [] }, tags) {
41
41
  if (selectTags.length > 0) {
42
42
  return !tags.some((tag) => selectTags.includes(tag))
43
43
  }
package/lib/ids.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Slug of heading text: lowercased, punctuation dropped, spaces as dashes.
3
+ *
4
+ * @param {string} text
5
+ * @returns {string}
6
+ */
7
+ export function slug(text: string): string;
8
+ /**
9
+ * Assign every rendered headline a unique id: its section's `CUSTOM_ID` when
10
+ * set, otherwise a slug of its text (`-1`, `-2`… on repeats). Sections skipped
11
+ * by `selectTags`/`excludeTags` are left out, as they are when rendering.
12
+ *
13
+ * @param {Nodes} tree
14
+ * @param {Pick<Config, 'selectTags' | 'excludeTags'>} config
15
+ */
16
+ export function headingIds(tree: Nodes, config: Pick<Config, "selectTags" | "excludeTags">): {
17
+ byNode: Map<Headline, string>;
18
+ byText: Map<string, string>;
19
+ };
20
+ import type { Nodes } from 'orga';
21
+ import type { Config } from './state.js';
22
+ import type { Headline } from 'orga';
23
+ //# sourceMappingURL=ids.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["ids.js"],"names":[],"mappings":"AAOA;;;;;GAKG;AACH,2BAHW,MAAM,GACJ,MAAM,CAQlB;AAED;;;;;;;GAOG;AACH,iCAHW,KAAK,UACL,IAAI,CAAC,MAAM,EAAE,YAAY,GAAG,aAAa,CAAC;;;EAiDpD;2BA3EiC,MAAM;4BACf,YAAY;8BADH,MAAM"}
package/lib/ids.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @import {Headline, Nodes} from 'orga'
3
+ * @import {Config} from './state.js'
4
+ */
5
+
6
+ import { shouldSkip } from './handlers/section.js'
7
+
8
+ /**
9
+ * Slug of heading text: lowercased, punctuation dropped, spaces as dashes.
10
+ *
11
+ * @param {string} text
12
+ * @returns {string}
13
+ */
14
+ export function slug(text) {
15
+ return text
16
+ .toLowerCase()
17
+ .trim()
18
+ .replace(/[^\p{L}\p{N}\s_-]/gu, '')
19
+ .replace(/\s+/g, '-')
20
+ }
21
+
22
+ /**
23
+ * Assign every rendered headline a unique id: its section's `CUSTOM_ID` when
24
+ * set, otherwise a slug of its text (`-1`, `-2`… on repeats). Sections skipped
25
+ * by `selectTags`/`excludeTags` are left out, as they are when rendering.
26
+ *
27
+ * @param {Nodes} tree
28
+ * @param {Pick<Config, 'selectTags' | 'excludeTags'>} config
29
+ */
30
+ export function headingIds(tree, config) {
31
+ /** @type {Array<{headline: Headline, customId: string | undefined}>} */
32
+ const headlines = []
33
+ collect(tree)
34
+
35
+ const used = new Set(headlines.map((h) => h.customId).filter(Boolean))
36
+ /** @type {Map<Headline, string>} */
37
+ const byNode = new Map()
38
+ /** @type {Map<string, string>} first heading with this text slug → its id */
39
+ const byText = new Map()
40
+
41
+ for (const { headline, customId } of headlines) {
42
+ const base = slug(text(headline))
43
+ let id = customId
44
+ if (!id && base) {
45
+ id = base
46
+ for (let n = 1; used.has(id); n++) id = `${base}-${n}`
47
+ used.add(id)
48
+ }
49
+ if (!id) continue
50
+ byNode.set(headline, id)
51
+ if (base && !byText.has(base)) byText.set(base, id)
52
+ }
53
+
54
+ return { byNode, byText }
55
+
56
+ /**
57
+ * @param {Nodes} node
58
+ * @param {Nodes} [parent]
59
+ */
60
+ function collect(node, parent) {
61
+ if (node.type === 'headline') {
62
+ const customId =
63
+ parent?.type === 'section' ? parent.properties.custom_id : undefined
64
+ headlines.push({
65
+ headline: node,
66
+ customId: typeof customId === 'string' ? customId : undefined
67
+ })
68
+ return
69
+ }
70
+ if (!('children' in node)) return
71
+ if (node.type === 'section') {
72
+ const headline = node.children.find((n) => n.type === 'headline')
73
+ if (headline && shouldSkip(config, headline.tags || [])) return
74
+ }
75
+ for (const child of node.children) collect(child, node)
76
+ }
77
+ }
78
+
79
+ const skip = new Set(['stars', 'todo', 'priority', 'tags'])
80
+
81
+ /**
82
+ * @param {Nodes} node
83
+ * @returns {string}
84
+ */
85
+ function text(node) {
86
+ if (skip.has(node.type)) return ''
87
+ if ('children' in node) return node.children.map(text).join('')
88
+ if (node.type === 'text') return node.value
89
+ return ''
90
+ }
package/lib/state.d.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  /**
2
- * @param {OastNodes} _tree
2
+ * @param {OastNodes} tree
3
3
  * @param {Partial<Config> | null | undefined} [options = {}]
4
4
  */
5
- export function createState(_tree: OastNodes, options?: Partial<Config> | null | undefined): {
5
+ export function createState(tree: OastNodes, options?: Partial<Config> | null | undefined): {
6
6
  one: (node: OastNodes, parent?: OastParent | undefined) => HastRoot | HastElementContent | HastElementContent[] | undefined;
7
+ ids: {
8
+ byNode: Map<import("orga").Headline, string>;
9
+ byText: Map<string, string>;
10
+ };
7
11
  all: (parent: OastNodes) => Array<HastElementContent>;
8
12
  handlers: Partial<Record<"newline" | "document" | "headline" | "paragraph" | "keyword" | "block" | "list" | "html" | "jsx" | "stars" | "todo" | "link" | "section" | "drawer" | "planning" | "table" | "hr" | "latex" | "footnote" | "table.row" | "table.hr" | "table.cell" | "list.item" | "text" | "footnote.reference" | "emptyLine" | "priority" | "tags" | "planning.keyword" | "planning.timestamp" | "list.item.tag" | "list.item.checkbox" | "list.item.bullet" | "table.columnSeparator" | "footnote.label" | "block.begin" | "block.end" | "latex.begin" | "latex.end" | "drawer.begin" | "drawer.end" | "comment" | "opening" | "closing" | "link.path", Handler>>;
9
13
  getAttrHtml: (node: OastNodes) => Record<string, string> | undefined;
10
14
  patch: <T extends HastNodes>(from: OastNodes, to: T) => T;
11
- /** @type {Config} */
12
15
  options: Config;
13
16
  };
14
17
  export type HastNodes = import("hast").Nodes;
@@ -1 +1 @@
1
- {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["state.js"],"names":[],"mappings":"AAiCA;;;GAGG;AACH,mCAHW,SAAS,YACT,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS;gBAqDjC,SAAS,WACT,UAAU,GAAG,SAAS;kBAzBtB,SAAS,KACP,KAAK,CAAC,kBAAkB,CAAC;;wBAgC3B,SAAS,KACP,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS;YAYtB,CAAC,SAAZ,SAAU,QACb,SAAS,MACT,CAAC,KACC,CAAC;IA/Db,qBAAqB;aAAV,MAAM;EAuElB;wBAxHY,OAAO,MAAM,EAAE,KAAK;yBAEpB,OAAO,MAAM,EAAE,MAAM;wBACrB,OAAO,MAAM,EAAE,KAAK;;cAKnB,QAAQ;gBACR,MAAM;cACN,CAAC,IAAI,EAAE,OAAO,MAAM,EAAE,IAAI,KAAK,MAAM;;;;gBACrC,MAAM,EAAE;;;;iBACR,MAAM,EAAE;;oBAIT,UAAU,CAAC,OAAO,WAAW,CAAC;8BAGhC,KAAK,QACL,GAAG,UACH,UAAU,GAAG,SAAS,KACpB,KAAK,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,GAAG,QAAQ,GAAG,SAAS;;;;uBAGrE,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;sCAxBe,MAAM;0DAAN,MAAM"}
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["state.js"],"names":[],"mappings":"AAkCA;;;GAGG;AACH,kCAHW,SAAS,YACT,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,GAAG,SAAS;gBA+EjC,SAAS,WACT,UAAU,GAAG,SAAS;;;;;kBAzBtB,SAAS,KACP,KAAK,CAAC,kBAAkB,CAAC;;wBAgC3B,SAAS,KACP,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS;YAYtB,CAAC,SAAZ,SAAU,QACb,SAAS,MACT,CAAC,KACC,CAAC;;EAQd;wBAnJY,OAAO,MAAM,EAAE,KAAK;yBAEpB,OAAO,MAAM,EAAE,MAAM;wBACrB,OAAO,MAAM,EAAE,KAAK;;cAKnB,QAAQ;gBACR,MAAM;cACN,CAAC,IAAI,EAAE,OAAO,MAAM,EAAE,IAAI,KAAK,MAAM;;;;gBACrC,MAAM,EAAE;;;;iBACR,MAAM,EAAE;;oBAIT,UAAU,CAAC,OAAO,WAAW,CAAC;8BAGhC,KAAK,QACL,GAAG,UACH,UAAU,GAAG,SAAS,KACpB,KAAK,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,GAAG,QAAQ,GAAG,SAAS;;;;uBAGrE,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;sCAxBe,MAAM;0DAAN,MAAM"}
package/lib/state.js CHANGED
@@ -30,37 +30,64 @@
30
30
 
31
31
  import { position } from 'unist-util-position'
32
32
  import { handlers as defaultHandlers } from './handlers/index.js'
33
+ import { headingIds, slug } from './ids.js'
33
34
 
34
35
  /**
35
- * @param {OastNodes} _tree
36
+ * @param {OastNodes} tree
36
37
  * @param {Partial<Config> | null | undefined} [options = {}]
37
38
  */
38
- export function createState(_tree, options = {}) {
39
+ export function createState(tree, options = {}) {
39
40
  /** @type {Handlers} */
40
41
  let handlers = { ...defaultHandlers }
41
42
  if (options?.handlers) {
42
43
  handlers = { ...handlers, ...options.handlers }
43
44
  }
44
45
 
46
+ /** @type {Config} */
47
+ const config = {
48
+ handlers,
49
+ linkTarget: '_self',
50
+ selectTags: [],
51
+ excludeTags: ['noexport'],
52
+ linkHref: defaultLinkHref,
53
+ ...options
54
+ }
55
+ const ids = headingIds(tree, config)
56
+
45
57
  const state = {
46
58
  one,
59
+ ids,
47
60
  all,
48
61
  handlers,
49
62
  getAttrHtml,
50
63
  patch,
51
- /** @type {Config} */
52
- options: {
53
- handlers,
54
- linkTarget: '_self',
55
- selectTags: [],
56
- excludeTags: ['noexport'],
57
- linkHref: (link) => link.path.value,
58
- ...options
59
- }
64
+ options: config
60
65
  }
61
66
 
62
67
  return state
63
68
 
69
+ /**
70
+ * @param {import('orga').Link} link
71
+ * @returns {string}
72
+ */
73
+ function defaultLinkHref(link) {
74
+ const { protocol, value, search } = link.path
75
+ if (protocol === 'internal' && value.startsWith('*')) {
76
+ const target = slug(value.slice(1))
77
+ return `#${ids.byText.get(target) ?? target}`
78
+ }
79
+ if (!protocol || protocol === 'internal') {
80
+ return value
81
+ }
82
+ if (protocol === 'file') {
83
+ return value + fragment(search)
84
+ }
85
+ if (protocol === 'http' || protocol === 'https') {
86
+ return value
87
+ }
88
+ return `${protocol}:${value}`
89
+ }
90
+
64
91
  /**
65
92
  * @param {OastNodes} parent
66
93
  * @returns {Array<HastElementContent>}
@@ -121,6 +148,20 @@ export function createState(_tree, options = {}) {
121
148
  }
122
149
  }
123
150
 
151
+ /**
152
+ * URL fragment for a `file:` link's search option: `*Heading` and `#custom-id`
153
+ * target a heading; other searches (line numbers, text) are dropped.
154
+ *
155
+ * @param {string | number | undefined} search
156
+ * @returns {string}
157
+ */
158
+ function fragment(search) {
159
+ if (typeof search !== 'string') return ''
160
+ if (search.startsWith('#')) return search
161
+ if (search.startsWith('*')) return `#${slug(search.slice(1))}`
162
+ return ''
163
+ }
164
+
124
165
  /** @type {Handler} */
125
166
  function unkownHandler(state, node) {
126
167
  if (node?.children) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oast-to-hast",
3
- "version": "4.5.2",
3
+ "version": "4.6.0",
4
4
  "description": "Transform OAST to HAST",
5
5
  "files": [
6
6
  "lib",