@hcengineering/text-markdown 0.7.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.
package/src/compare.ts ADDED
@@ -0,0 +1,119 @@
1
+ //
2
+ // Copyright © 2025 Hardcore Engineering Inc.
3
+ //
4
+ // Licensed under the Eclipse Public License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License. You may
6
+ // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ //
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+
16
+ /**
17
+ * Calculate Sørensen–Dice coefficient
18
+ */
19
+ export function calcSørensenDiceCoefficient (a: string, b: string): number {
20
+ const first = a.replace(/\s+/g, '')
21
+ const second = b.replace(/\s+/g, '')
22
+
23
+ if (first === second) return 1 // identical or empty
24
+ if (first.length < 2 || second.length < 2) return 0 // if either is a 0-letter or 1-letter string
25
+
26
+ const firstBigrams = new Map<string, number>()
27
+ for (let i = 0; i < first.length - 1; i++) {
28
+ const bigram = first.substring(i, i + 2)
29
+ const count = (firstBigrams.get(bigram) ?? 0) + 1
30
+
31
+ firstBigrams.set(bigram, count)
32
+ }
33
+
34
+ let intersectionSize = 0
35
+ for (let i = 0; i < second.length - 1; i++) {
36
+ const bigram = second.substring(i, i + 2)
37
+ const count = firstBigrams.get(bigram) ?? 0
38
+
39
+ if (count > 0) {
40
+ firstBigrams.set(bigram, count - 1)
41
+ intersectionSize++
42
+ }
43
+ }
44
+
45
+ return (2.0 * intersectionSize) / (first.length + second.length - 2)
46
+ }
47
+
48
+ /**
49
+ * Perform markdown diff/comparison to understand do we have a major differences.
50
+ */
51
+ export function isMarkdownsEquals (source1: string, source2: string): boolean {
52
+ const normalized1 = normalizeMarkdown(source1)
53
+ const normalized2 = normalizeMarkdown(source2)
54
+ return normalized1 === normalized2
55
+ }
56
+
57
+ export function normalizeMarkdown (source: string): string {
58
+ const tagRegex = /<(\w+)([^>]*?)(\/?)>/g
59
+ const attrRegex = /(\w+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g
60
+
61
+ // Normalize line endings to LF
62
+ source = source.replace(/\r?\n/g, '\n')
63
+
64
+ // Remove extra blank lines
65
+ source = source
66
+ .split('\n')
67
+ .map((it) => it.trimEnd())
68
+ .filter((it) => it.length > 0)
69
+ .join('\n')
70
+
71
+ // Normalize HTML tags
72
+ source = source.replace(tagRegex, (match, tagName, attributes) => {
73
+ const attrs: Record<string, string> = {}
74
+
75
+ let attrMatch = attrRegex.exec(attributes)
76
+ while (attrMatch !== null) {
77
+ const attrName = attrMatch[1]
78
+ const attrValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? ''
79
+ attrs[attrName] = attrValue
80
+ attrMatch = attrRegex.exec(attributes)
81
+ }
82
+
83
+ // Sort attributes by name for consistent order
84
+ const sortedAttrs = Object.keys(attrs)
85
+ .sort()
86
+ .map((key) => {
87
+ const value = attrs[key]
88
+ return value !== '' ? `${key}="${value}"` : key
89
+ })
90
+ .join(' ')
91
+
92
+ // Normalize to self-closing format for void elements
93
+ const voidElements = [
94
+ 'img',
95
+ 'br',
96
+ 'hr',
97
+ 'input',
98
+ 'meta',
99
+ 'area',
100
+ 'base',
101
+ 'col',
102
+ 'embed',
103
+ 'link',
104
+ 'param',
105
+ 'source',
106
+ 'track',
107
+ 'wbr'
108
+ ]
109
+ const isVoidElement = voidElements.includes(tagName.toLowerCase())
110
+
111
+ if (sortedAttrs !== '') {
112
+ return isVoidElement ? `<${tagName} ${sortedAttrs} />` : `<${tagName} ${sortedAttrs}>`
113
+ } else {
114
+ return isVoidElement ? `<${tagName} />` : `<${tagName}>`
115
+ }
116
+ })
117
+
118
+ return source
119
+ }
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ //
2
+ // Copyright © 2025 Hardcore Engineering Inc.
3
+ //
4
+ // Licensed under the Eclipse Public License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License. You may
6
+ // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ //
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+
16
+ import { MarkupNode } from '@hcengineering/text-core'
17
+ import { MarkdownParser } from './parser'
18
+ import { MarkdownState, storeMarks, storeNodes } from './serializer'
19
+
20
+ export * from './compare'
21
+ export * from './parser'
22
+ export * from './serializer'
23
+
24
+ /** @public */
25
+ export interface MarkdownOptions {
26
+ refUrl?: string
27
+ imageUrl?: string
28
+ }
29
+
30
+ /** @public */
31
+ export function markupToMarkdown (markup: MarkupNode, options?: MarkdownOptions): string {
32
+ const refUrl = options?.refUrl ?? 'ref://'
33
+ const imageUrl = options?.imageUrl ?? 'image://'
34
+
35
+ const state = new MarkdownState(storeNodes, storeMarks, { tightLists: true, refUrl, imageUrl })
36
+ state.renderContent(markup)
37
+ return state.out
38
+ }
39
+
40
+ /** @public */
41
+ export function markdownToMarkup (markdown: string, options?: MarkdownOptions): MarkupNode {
42
+ const refUrl = options?.refUrl ?? 'ref://'
43
+ const imageUrl = options?.imageUrl ?? 'image://'
44
+
45
+ const parser = new MarkdownParser({ refUrl, imageUrl })
46
+ return parser.parse(markdown ?? '')
47
+ }
package/src/marks.ts ADDED
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright © 2025 Hardcore Engineering Inc.
3
+ //
4
+ // Licensed under the Eclipse Public License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License. You may
6
+ // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ //
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+
16
+ import { MarkupMark, MarkupMarkType } from '@hcengineering/text-core'
17
+ import { deepEqual } from 'fast-equals'
18
+
19
+ export function markAttrs (mark: MarkupMark): Record<string, string> {
20
+ return mark.attrs ?? {}
21
+ }
22
+
23
+ export function isInSet (mark: MarkupMark, marks: MarkupMark[]): boolean {
24
+ return marks.find((m) => markEq(mark, m)) !== undefined
25
+ }
26
+
27
+ export function addToSet (mark: MarkupMark, marks: MarkupMark[]): MarkupMark[] {
28
+ const m = marks.find((m) => markEq(mark, m))
29
+ if (m !== undefined) {
30
+ // We already have mark
31
+ return marks
32
+ }
33
+ return [...marks, mark]
34
+ }
35
+
36
+ export function removeFromSet (markType: MarkupMarkType, marks: MarkupMark[]): MarkupMark[] {
37
+ return marks.filter((m) => m.type !== markType)
38
+ }
39
+
40
+ export function sameSet (a?: MarkupMark[], b?: MarkupMark[]): boolean {
41
+ return deepEqual(a, b)
42
+ }
43
+
44
+ export function markEq (first: MarkupMark, other: MarkupMark): boolean {
45
+ return deepEqual(first, other)
46
+ }
package/src/node.ts ADDED
@@ -0,0 +1,24 @@
1
+ //
2
+ // Copyright © 2025 Hardcore Engineering Inc.
3
+ //
4
+ // Licensed under the Eclipse Public License, Version 2.0 (the "License");
5
+ // you may not use this file except in compliance with the License. You may
6
+ // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ //
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+
16
+ import { Attrs, MarkupNode } from '@hcengineering/text-core'
17
+
18
+ export function nodeContent (node: MarkupNode): MarkupNode[] {
19
+ return node?.content ?? []
20
+ }
21
+
22
+ export function nodeAttrs (node: MarkupNode): Attrs {
23
+ return node.attrs ?? {}
24
+ }