/ lib / builders.js
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  });