builders.js
1 import * as btree from './btree.js'; 2 import { printType } from './print.js'; 3 import { 4 DoctypeTag, 5 OpenNodeTag, 6 CloseNodeTag, 7 ReferenceTag, 8 ShiftTag, 9 GapTag, 10 NullTag, 11 ArrayInitializerTag, 12 LiteralTag, 13 EmbeddedObject, 14 EmbeddedTag, 15 TokenGroup, 16 EmbeddedMatcher, 17 EmbeddedRegex, 18 EmbeddedNode, 19 } from './symbols.js'; 20 21 const { freeze } = Object; 22 const { isArray } = Array; 23 24 const isObject = (val) => val !== null && typeof value !== 'object'; 25 26 function* relatedNodes(properties) { 27 for (const value of Object.values(properties)) { 28 if (isArray(value)) { 29 for (let value of btree.traverse(value)) { 30 yield value.node; 31 } 32 } else { 33 yield value.node; 34 } 35 } 36 } 37 38 const find = (predicate, iterable) => { 39 for (const value of iterable) { 40 if (predicate(value)) return value; 41 } 42 }; 43 44 export const buildEmbeddedObject = (expr) => { 45 if (!isObject(expr)) throw new Error(); 46 return freeze({ type: EmbeddedObject, value: expr }); 47 }; 48 49 export const buildEmbeddedNode = (node) => { 50 if (!isObject(node)) throw new Error(); 51 return freeze({ type: EmbeddedNode, value: node }); 52 }; 53 54 export const buildEmbeddedMatcher = (node) => { 55 if (!isObject(node)) throw new Error(); 56 return freeze({ type: EmbeddedMatcher, value: node }); 57 }; 58 59 export const buildEmbeddedRegex = (node) => { 60 if (!isObject(node)) throw new Error(); 61 return freeze({ type: EmbeddedRegex, value: node }); 62 }; 63 64 export const buildEmbeddedTag = (tag) => { 65 if (!isObject(tag)) throw new Error(); 66 return freeze({ type: EmbeddedTag, value: tag }); 67 }; 68 69 export const buildEffect = (value) => { 70 return freeze({ type: 'Effect', value }); 71 }; 72 73 export const buildWriteEffect = (text, options = {}) => { 74 return buildEffect( 75 freeze({ 76 verb: 'write', 77 value: buildEmbeddedObject(freeze({ text, options: buildEmbeddedObject(freeze(options)) })), 78 }), 79 ); 80 }; 81 82 export const buildAnsiPushEffect = (spans = '') => { 83 return buildEffect( 84 freeze({ 85 verb: 'ansi-push', 86 value: buildEmbeddedObject( 87 freeze({ spans: spans === '' ? freeze([]) : freeze(spans.split(' ')) }), 88 ), 89 }), 90 ); 91 }; 92 93 export const buildAnsiPopEffect = () => { 94 return buildEffect(freeze({ verb: 'ansi-pop', value: undefined })); 95 }; 96 97 export const buildTokenGroup = (tokens) => { 98 return freeze({ type: TokenGroup, value: tokens }); 99 }; 100 101 export const buildCall = (verb, ...args) => { 102 return { verb, arguments: args }; 103 }; 104 105 export const buildBeginningOfStreamToken = () => { 106 return freeze({ type: Symbol.for('@bablr/beginning-of-stream'), value: undefined }); 107 }; 108 109 export const buildReferenceTag = (name, isArray = false, flags = referenceFlags, index = null) => { 110 if (name == null || !/[a-zA-Z.#@]/.test(name)) throw new Error('reference must have a name'); 111 if (index != null && !Number.isFinite(index)) throw new Error(); 112 return freeze({ type: ReferenceTag, value: freeze({ name, isArray, index, flags }) }); 113 }; 114 115 export const buildNullTag = () => { 116 return freeze({ type: NullTag, value: undefined }); 117 }; 118 119 export const buildArrayInitializerTag = () => { 120 return freeze({ type: ArrayInitializerTag, value: undefined }); 121 }; 122 123 export const buildGapTag = () => { 124 return freeze({ type: GapTag, value: undefined }); 125 }; 126 127 export const buildShiftTag = (index) => { 128 if (!Number.isFinite(index)) throw new Error(); 129 return freeze({ type: ShiftTag, value: freeze({ index }) }); 130 }; 131 132 export const buildDoctypeTag = (attributes = {}) => { 133 return freeze({ 134 type: DoctypeTag, 135 value: { doctype: 'cstml', version: 0, attributes: freeze(attributes) }, 136 }); 137 }; 138 139 export const buildOpenNodeTag = ( 140 flags = nodeFlags, 141 language = null, 142 type = null, 143 attributes = {}, 144 ) => { 145 if (printType(type).startsWith('https://')) throw new Error(); 146 147 return freeze({ 148 type: OpenNodeTag, 149 value: freeze({ 150 flags: freeze(flags), 151 language, 152 type: isString(type) ? Symbol.for(type) : type, 153 attributes, 154 }), 155 }); 156 }; 157 158 export const buildCloseNodeTag = () => { 159 return freeze({ type: CloseNodeTag, value: undefined }); 160 }; 161 162 const isString = (val) => typeof val === 'string'; 163 164 export const buildLiteralTag = (value) => { 165 if (!isString(value)) throw new Error('invalid literal'); 166 return freeze({ type: LiteralTag, value }); 167 }; 168 169 const flagsWithGap = new WeakMap(); 170 171 export const getFlagsWithGap = (flags) => { 172 let gapFlags = flagsWithGap.get(flags); 173 if (!gapFlags) { 174 gapFlags = { ...flags, hasGap: true }; 175 flagsWithGap.set(flags, gapFlags); 176 } 177 return gapFlags; 178 }; 179 180 export const nodeFlags = freeze({ 181 token: false, 182 hasGap: false, 183 }); 184 185 const hasGap = (properties) => { 186 return find((node) => node.flags.hasGap, relatedNodes(properties)); 187 }; 188 189 const getFlags = (flags, properties) => { 190 if (!hasGap(properties)) { 191 return flags; 192 } else { 193 return getFlagsWithGap(flags); 194 } 195 }; 196 197 export const tokenFlags = freeze({ 198 token: true, 199 hasGap: false, 200 }); 201 202 export const referenceFlags = freeze({ 203 expression: false, 204 hasGap: false, 205 }); 206 207 export const gapReferenceFlags = freeze({ 208 expression: false, 209 hasGap: true, 210 }); 211 212 export const expressionReferenceFlags = freeze({ 213 expression: true, 214 hasGap: false, 215 });