All files / json-crdt-extensions/peritext/registry SliceRegistry.ts

95.23% Statements 40/42
71.42% Branches 15/21
87.5% Functions 7/8
95% Lines 38/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 15762x 62x                   62x           246x                 103560x                         103560x         103560x                                 103560x               103560x                                   103560x                                       62x 3452x 3452x     103560x 103560x 103560x 103560x 13808x 24164x 24164x 24164x 24164x     103560x 103560x       192x 192x                 235x 235x 235x 181x 181x 181x 181x 65x 65x 65x   181x       54x      
import {SliceBehavior, type SliceTypeCon} from '../slice/constants';
import {CommonSliceType} from '../slice';
import type {PeritextMlElement} from '../block/types';
import type {NodeBuilder} from '../../../json-crdt-patch';
import type {JsonMlElement} from 'very-small-parser/lib/html/json-ml/types';
import type {FromHtmlConverter, ToHtmlConverter} from './types';
import type {JsonNodeView} from '../../../json-crdt/nodes';
import type {SchemaToJsonNode} from '../../../json-crdt/schema/types';
 
export type TagType = SliceTypeCon | number | string;
 
export class SliceRegistryEntry<
  Behavior extends SliceBehavior = SliceBehavior,
  Tag extends TagType = TagType,
  Schema extends NodeBuilder = NodeBuilder,
> {
  public isInline(): boolean {
    return this.behavior !== SliceBehavior.Marker;
  }
 
  constructor(
    /**
     * Specifies whether the slice is an inline or block element. And if it is
     * an inline element, whether multiple instances of the same tag are allowed
     * to be applied to a range of tex - "Many", or only one instance - "One".
     */
    public readonly behavior: Behavior,
 
    /**
     * The tag name of this slice. The tag is one step in the type path of the
     * slice. For example, below is a type path composed of three steps:
     *
     * ```js
     * ['ul', 'li', 'p']
     * ```
     *
     * Tag types are normally numbers of type {@link SliceTypeCon}, however,
     * they can also be any arbitrary strings or numbers.
     */
    public readonly tag: Tag,
 
    /**
     * Default expected schema of the slice data.
     */
    public readonly schema: Schema,
 
    /**
     * This property is relevant only for block split markers. It specifies
     * whether the block split marker is a container for other block elements.
     *
     * For example, a `blockquote` is a container for `paragraph` elements,
     * however, a `paragraph` is not a container (it can only contain inline
     * elements).
     *
     * If the marker slice is of the container sort, they tag can appear in the
     * path steps of the type:
     *
     * ```
     *
     * ```
     */
    public readonly container: boolean = false,
 
    /**
     * Converts a node of this type to HTML representation: returns the HTML tag
     * and attributes. The method receives {@link PeritextMlElement} as an
     * argument, which is a tuple of internal HTML-like representation of the
     * node.
     */
    public readonly toHtml:
      | ToHtmlConverter<
          PeritextMlElement<
            Tag,
            JsonNodeView<SchemaToJsonNode<Schema>>,
            Behavior extends SliceBehavior.Marker ? false : true
          >
        >
      | undefined = void 0,
 
    /**
     * Specifies a mapping of converters from HTML {@link JsonMlElement} to
     * {@link PeritextMlElement}. This way a slice type can specify multiple
     * HTML tags that are converted to the same slice type.
     *
     * For example, both, `<b>` and `<strong>` tags can be converted to the
     * {@link SliceTypeCon.b} slice type.
     */
    public readonly fromHtml?: {
      [htmlTag: string]: FromHtmlConverter<
        PeritextMlElement<
          Tag,
          JsonNodeView<SchemaToJsonNode<Schema>>,
          Behavior extends SliceBehavior.Marker ? false : true
        >
      >;
    },
  ) {}
}
 
/**
 * Slice registry contains a record of possible inline an block formatting
 * annotations. Each entry in the registry is a {@link SliceRegistryEntry} that
 * specifies the behavior, tag, and other properties of the slice.
 *
 * @todo Consider moving the registry under the `/transfer` directory. Or maybe
 * `/slices` directory.
 */
export class SliceRegistry {
  private map: Map<TagType, SliceRegistryEntry> = new Map();
  private _fromHtml: Map<string, [entry: SliceRegistryEntry, converter: FromHtmlConverter][]> = new Map();
 
  public add(entry: SliceRegistryEntry<any, any, any>): void {
    const {tag, fromHtml} = entry;
    this.map.set(tag, entry);
    const _fromHtml = this._fromHtml;
    if (fromHtml) {
      for (const htmlTag in fromHtml) {
        const converter = fromHtml[htmlTag];
        const converters = _fromHtml.get(htmlTag) ?? [];
        converters.push([entry, converter]);
        _fromHtml.set(htmlTag, converters);
      }
    }
    const tagStr = CommonSliceType[tag as SliceTypeCon];
    if (tagStr && typeof tagStr === 'string') _fromHtml.set(tagStr, [[entry, () => [tag, null]]]);
  }
 
  public isContainer(tag: TagType): boolean {
    const entry = this.map.get(tag);
    return entry?.container ?? false;
  }
 
  public toHtml(el: PeritextMlElement): ReturnType<ToHtmlConverter<any>> | undefined {
    const entry = this.map.get(el[0]);
    return entry?.toHtml ? entry?.toHtml(el) : void 0;
  }
 
  public fromHtml(el: JsonMlElement): PeritextMlElement | undefined {
    const tag = el[0] + '';
    const converters = this._fromHtml.get(tag);
    if (converters) {
      for (const [entry, converter] of converters) {
        const result = converter(el);
        if (result) {
          if (entry.isInline()) {
            const attr = result[1] ?? (result[1] = {});
            attr.inline = entry.isInline();
            attr.behavior = !attr.inline ? SliceBehavior.Marker : (entry.behavior ?? SliceBehavior.Many);
          }
          return result;
        }
      }
    }
    return;
  }
}