// Copyright (c) Rotorz Limited and portions by original markdown-it-video authors // Licensed under the MIT license. See LICENSE file in the project root. import StateBlock from 'markdown-it/lib/rules_block/state_block'; /** * Interface for the service configuration used by the tokenizer. */ export interface EmbedService { extractVideoID: (reference: string) => string | null; [key: string]: any; } export interface EmbedServiceMap { [key: string]: EmbedService; } interface Pointer { line: number; pos: number; prevPos?: number; } const SYNTAX_CHARS = ['@', '[', ']', '(', ')']; const SYNTAX_CODES = SYNTAX_CHARS.map(char => char.charCodeAt(0)); /** * Advances the pointer to the next occurrence of a specific symbol. */ function advanceToSymbol(state: StateBlock, endLine: number, symbol: string, pointer: Pointer): boolean { let symbolLine = pointer.line; let symbolIndex = state.src.indexOf(symbol, pointer.pos); if (symbolIndex === -1) return false; let maxPos = state.eMarks[pointer.line]; while (symbolIndex >= maxPos) { ++symbolLine; maxPos = state.eMarks[symbolLine]; if (symbolLine >= endLine) return false; } pointer.prevPos = pointer.pos; pointer.pos = symbolIndex; pointer.line = symbolLine; return true; } /** * Factory function to create the tokenizer with access to configured services. */ export function createTokenizer(services: EmbedServiceMap) { return function tokenizer(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean { let startPos = state.bMarks[startLine] + state.tShift[startLine]; let maxPos = state.eMarks[startLine]; let pointer: Pointer = { line: startLine, pos: startPos }; // Block embed must be at start of input or the previous line must be blank. if (startLine !== 0) { let prevLineStartPos = state.bMarks[startLine - 1] + state.tShift[startLine - 1]; let prevLineMaxPos = state.eMarks[startLine - 1]; if (prevLineMaxPos > prevLineStartPos) return false; } // Identify as being a potential block embed: @[ if (maxPos - startPos < 2) return false; if (SYNTAX_CODES[0] !== state.src.charCodeAt(pointer.pos++)) return false; // Read service name from within square brackets. if (SYNTAX_CODES[1] !== state.src.charCodeAt(pointer.pos++)) return false; if (!advanceToSymbol(state, endLine, ']', pointer)) return false; let serviceName = state.src .substring(pointer.prevPos!, pointer.pos) .trim() .toLowerCase(); pointer.pos++; // Lookup service; if unknown, then this is not a known embed! const service = services[serviceName]; if (!service) return false; // Read embed reference from within parenthesis: (reference) if (SYNTAX_CODES[3] !== state.src.charCodeAt(pointer.pos++)) return false; if (!advanceToSymbol(state, endLine, ')', pointer)) return false; let videoReference = state.src .substring(pointer.prevPos!, pointer.pos) .trim(); pointer.pos++; // Do not recognize as block element when there is trailing text. maxPos = state.eMarks[pointer.line]; let trailingText = state.src .substring(pointer.pos, maxPos) .trim(); if (trailingText !== "") return false; // Block embed must be at end of input or the next line must be blank. if (endLine !== pointer.line + 1) { const nextLineStartPos = state.bMarks[pointer.line + 1] + state.tShift[pointer.line + 1]; const nextLineMaxPos = state.eMarks[pointer.line + 1]; if (nextLineMaxPos > nextLineStartPos) return false; } if (pointer.line >= endLine) return false; if (!silent) { const token = state.push('video', 'div', 0); token.markup = state.src.slice(startPos, pointer.pos); token.block = true; (token as any).info = { serviceName, service, videoReference, videoID: service.extractVideoID(videoReference), }; token.map = [startLine, pointer.line + 1]; state.line = pointer.line + 1; } return true; }; }