-
Notifications
You must be signed in to change notification settings - Fork 42
/
transform.ts
129 lines (118 loc) · 3.6 KB
/
transform.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import { OperationTransform } from '@stencila/stencila'
import { ElementId } from '../../types'
import { assert, isElement, isText, panic } from '../checks'
import { resolveNode } from './resolve'
/**
* Apply a transform operation
*
* Transform operations allow for a lightweight diff where only the type
* of the node has changed. See the `diff_transform` function in `rust/src/patches/inlines.rs`
* This function should be able to apply all the transforms potentially
* generated on the server.
*
* Asserts that the element type is as expect in the `from` property of the operation.
*/
export function applyTransform(
op: OperationTransform,
target?: ElementId
): void {
const { address, from, to } = op
const node = resolveNode(address, target)
if (parent === undefined) {
console.warn(
`Unable to resolve address '${address.join(
','
)}'; 'Transform' operation will be ignored'`
)
} else if (isText(node)) applyTransformString(node, from, to)
else if (isElement(node)) applyTransformElem(node, from, to)
else throw panic(`Unexpected transform node`)
}
/**
* Tags used for various node types
*/
const TYPE_TAGS: Record<string, string> = {
Emphasis: 'em',
Delete: 'del',
Strong: 'strong',
Subscript: 'sub',
Superscript: 'sup',
}
/**
* Types corresponding to various element tags
*/
const TAGS_TYPE: Record<string, string> = {
em: 'Emphasis',
del: 'Delete',
strong: 'Strong',
sub: 'Subscript',
sup: 'Superscript',
}
/**
* Apply a transform operation to a string
*/
export function applyTransformString(
text: Text,
from: string,
to: string
): void {
assert(from === 'String', `Expected transform from type String, got ${from}`)
const tag = TYPE_TAGS[to]
if (tag === undefined) {
throw panic(`Unexpected transform to type ${to}`)
}
const elem = document.createElement(tag)
elem.textContent = text.textContent
text.replaceWith(elem)
}
/**
* Apply a transform operation to an element
*/
export function applyTransformElem(
elem: Element,
from: string,
to: string
): void {
// For syntheticaly created transform operations e.g. those for
// dealing with a change in heading depth, the from field is empty.
// For others, check that the current tag of the element is as expected.
if (from !== '') {
const tag = elem.tagName.toLowerCase()
const expectedFrom = TAGS_TYPE[tag]
if (expectedFrom === undefined) throw panic(`Unhandled from tag ${tag}`)
if (expectedFrom !== from)
throw panic(
`Expected transform from type ${expectedFrom} for tag ${tag}, got ${from}`
)
}
if (to === 'String') {
const text = document.createTextNode(elem.textContent ?? '')
elem.replaceWith(text)
} else {
// For normal transform operation the `to` field is the name of the
// type (starting with a capital letter). For others, it is the new tag name.
if (/^[A-Z]/.test(to)) {
const tag = TYPE_TAGS[to]
if (tag === undefined) throw panic(`Unhandled to type ${to}`)
changeTagName(elem, tag, to)
} else {
changeTagName(elem, to)
}
}
}
/**
* Change the tag name of an element
*/
export function changeTagName(elem: Element, tag: string, type?: string): void {
const changed = document.createElement(tag)
changed.innerHTML = elem.innerHTML
for (let index = 0; index < elem.attributes.length; index++) {
const attr = elem.attributes[index] as Attr
if (attr.name === 'itemtype' && type !== undefined) {
changed.setAttribute(attr.name, `https://schema.stenci.la/${type}`)
} else {
changed.setAttribute(attr.name, attr.value)
}
}
elem.replaceWith(changed)
}