/
formatting-element-list.ts
159 lines (124 loc) · 4.9 KB
/
formatting-element-list.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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import type { Attribute, TagToken } from '../common/token.js';
import type { TreeAdapter, TreeAdapterTypeMap } from '../tree-adapters/interface';
//Const
const NOAH_ARK_CAPACITY = 3;
export enum EntryType {
Marker,
Element,
}
interface MarkerEntry {
type: EntryType.Marker;
}
export interface ElementEntry<T extends TreeAdapterTypeMap> {
type: EntryType.Element;
element: T['element'];
token: TagToken;
}
export type Entry<T extends TreeAdapterTypeMap> = MarkerEntry | ElementEntry<T>;
const MARKER: MarkerEntry = { type: EntryType.Marker };
//List of formatting elements
export class FormattingElementList<T extends TreeAdapterTypeMap> {
entries: Entry<T>[] = [];
bookmark: Entry<T> | null = null;
constructor(private treeAdapter: TreeAdapter<T>) {}
//Noah Ark's condition
//OPTIMIZATION: at first we try to find possible candidates for exclusion using
//lightweight heuristics without thorough attributes check.
private _getNoahArkConditionCandidates(
newElement: T['element'],
neAttrs: Attribute[]
): { idx: number; attrs: Attribute[] }[] {
const candidates = [];
const neAttrsLength = neAttrs.length;
const neTagName = this.treeAdapter.getTagName(newElement);
const neNamespaceURI = this.treeAdapter.getNamespaceURI(newElement);
for (let i = 0; i < this.entries.length; i++) {
const entry = this.entries[i];
if (entry.type === EntryType.Marker) {
break;
}
const { element } = entry;
if (
this.treeAdapter.getTagName(element) === neTagName &&
this.treeAdapter.getNamespaceURI(element) === neNamespaceURI
) {
const elementAttrs = this.treeAdapter.getAttrList(element);
if (elementAttrs.length === neAttrsLength) {
candidates.push({ idx: i, attrs: elementAttrs });
}
}
}
return candidates;
}
private _ensureNoahArkCondition(newElement: T['element']): void {
if (this.entries.length < NOAH_ARK_CAPACITY) return;
const neAttrs = this.treeAdapter.getAttrList(newElement);
const candidates = this._getNoahArkConditionCandidates(newElement, neAttrs);
if (candidates.length < NOAH_ARK_CAPACITY) return;
//NOTE: build attrs map for the new element, so we can perform fast lookups
const neAttrsMap = new Map(neAttrs.map((neAttr: Attribute) => [neAttr.name, neAttr.value]));
let validCandidates = 0;
//NOTE: remove bottommost candidates, until Noah's Ark condition will not be met
for (let i = 0; i < candidates.length; i++) {
const candidate = candidates[i];
// We know that `candidate.attrs.length === neAttrs.length`
if (candidate.attrs.every((cAttr) => neAttrsMap.get(cAttr.name) === cAttr.value)) {
validCandidates += 1;
if (validCandidates >= NOAH_ARK_CAPACITY) {
this.entries.splice(candidate.idx, 1);
}
}
}
}
//Mutations
insertMarker(): void {
this.entries.unshift(MARKER);
}
pushElement(element: T['element'], token: TagToken): void {
this._ensureNoahArkCondition(element);
this.entries.unshift({
type: EntryType.Element,
element,
token,
});
}
insertElementAfterBookmark(element: T['element'], token: TagToken): void {
const bookmarkIdx = this.entries.indexOf(this.bookmark!);
this.entries.splice(bookmarkIdx, 0, {
type: EntryType.Element,
element,
token,
});
}
removeEntry(entry: Entry<T>): void {
const entryIndex = this.entries.indexOf(entry);
if (entryIndex >= 0) {
this.entries.splice(entryIndex, 1);
}
}
/**
* Clears the list of formatting elements up to the last marker.
*
* @see https://html.spec.whatwg.org/multipage/parsing.html#clear-the-list-of-active-formatting-elements-up-to-the-last-marker
*/
clearToLastMarker(): void {
const markerIdx = this.entries.indexOf(MARKER);
if (markerIdx >= 0) {
this.entries.splice(0, markerIdx + 1);
} else {
this.entries.length = 0;
}
}
//Search
getElementEntryInScopeWithTagName(tagName: string): ElementEntry<T> | null {
const entry = this.entries.find(
(entry) => entry.type === EntryType.Marker || this.treeAdapter.getTagName(entry.element) === tagName
);
return entry && entry.type === EntryType.Element ? entry : null;
}
getElementEntry(element: T['element']): ElementEntry<T> | undefined {
return this.entries.find(
(entry): entry is ElementEntry<T> => entry.type === EntryType.Element && entry.element === element
);
}
}