/* eslint max-lines: ["error", { "max": 450 }] */ import { HighlightStyle } from '@codemirror/language'; import { tags } from '@lezer/highlight'; import { legacyTokenizeCustomMarkdown } from './legacyCustomHighlight'; import { Decoration, ViewPlugin, } from '@codemirror/view'; import { syntaxTree, ensureSyntaxTree } from '@codemirror/language'; // Making the tokens const customTags = { pageLine : 'pageLine', // .cm-pageLine snippetLine : 'snippetLine', // .cm-snippetLine columnSplit : 'columnSplit', // .cm-columnSplit block : 'block', // .cm-block inlineBlock : 'inline-block', // .cm-inline-block injection : 'injection', // .cm-injection emoji : 'emoji', // .cm-emoji superscript : 'superscript', // .cm-superscript subscript : 'subscript', // .cm-subscript definitionList : 'definitionList', // .cm-definitionList definitionTerm : 'definitionTerm', // .cm-definitionTerm definitionDesc : 'definitionDesc', // .cm-definitionDesc definitionColon : 'definitionColon', // .cm-definitionColon strikethrough : 'strikethrough', // .cm-strikethrough //CSS variable : 'variable', }; function tokenizeCustomMarkdown(text) { const tokens = []; const lines = text.split('\n'); //tokens without a `from` or `to` are interpreted by the custom plugin as line tokens lines.forEach((lineText, lineNumber)=>{ // --- Page / snippet lines --- if(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m.test(lineText)) tokens.push({ line: lineNumber, type: customTags.pageLine }); if(/^\\snippet\ .*$/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.snippetLine }); if(/^\\column(?:break)?$/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.columnSplit }); // --- Emoji --- if(/:.\w+?:/.test(lineText)) { const emojiRegex = /(:\w+?:)/g; let match; while ((match = emojiRegex.exec(lineText)) !== null) { tokens.push({ line : lineNumber, type : customTags.emoji, from : match.index, to : match.index + match[0].length, }); } } // --- Superscript / Subscript --- if(/\^/.test(lineText)) { let startIndex = lineText.indexOf('^'); const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy; const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy; while (startIndex >= 0) { superRegex.lastIndex = subRegex.lastIndex = startIndex; let match = subRegex.exec(lineText); let type = customTags.subscript; if(!match) { match = superRegex.exec(lineText); type = customTags.superscript; } if(match) { tokens.push({ line : lineNumber, type, from : match.index, to : match.index + match[0].length, }); } startIndex = lineText.indexOf( '^', Math.max(startIndex + 1, superRegex.lastIndex || 0, subRegex.lastIndex || 0), ); } } // --- Strikethrough --- if(/\~/.test(lineText)) { const strikethroughRegex = /~(?!\s)(.+?)(? 0 && lineText.trim().length > 0) { tokens.push({ line : startLine, type : customTags.definitionList, }); // term tokens.push({ line : startLine, type : customTags.definitionTerm, from : 0, to : lineText.length, }); // definitions defs.forEach((d)=>{ tokens.push({ line : d.line, type : customTags.definitionList, }); tokens.push({ line : d.line, type : customTags.definitionColon, from : 0, to : d.colons.length, }); tokens.push({ line : d.line, type : customTags.definitionDesc, from : d.colons.length, to : d.colons.length + d.desc?.length, }); }); } } if(lineText.includes('{') && lineText.includes('}')) { const injectionRegex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gmd; let match; while ((match = injectionRegex.exec(lineText)) !== null) { tokens.push({ line : lineNumber, from : match.indices[1][0], to : match.indices[1][1], type : customTags.injection, }); } } if(lineText.includes('{{') && lineText.includes('}}')) { // Inline blocks: single-line {{…}} const spanRegex = /{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *|}}/g; let match; let blockCount = 0; while ((match = spanRegex.exec(lineText)) !== null) { if(match[0].startsWith('{{')) { blockCount += 1; } else { blockCount -= 1; } if(blockCount < 0) { blockCount = 0; continue; } tokens.push({ line : lineNumber, from : match.index, to : match.index + match[0].length, type : customTags.inlineBlock, }); } } else if(lineText.trimLeft().startsWith('{{') || lineText.trimLeft().startsWith('}}')) { // Highlight block divs {{\n Content \n}} let endCh = lineText.length + 1; const match = lineText.match( /^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/, ); if(match) endCh = match.index + match[0].length; tokens.push({ line: lineNumber, type: customTags.block }); } }); return tokens; } function tokenizeCustomCSS(text) { const tokens = []; const lines = text.split('\n'); lines.forEach((lineText, lineNumber)=>{ if(/--[a-zA-Z0-9-_]+/gm.test(lineText)) { const varRegex =/--[a-zA-Z0-9-_]+/gm; let match; while ((match = varRegex.exec(lineText)) !== null) { tokens.push({ line : lineNumber, from : match.index +1, to : match.index + match.length[1] +1, type : customTags.varProperty, }); } } }); return tokens; } //assign classes to tags provided by lezer, not unlike the function above export const customHighlightStyle = HighlightStyle.define([ { tag: tags.heading, class: 'cm-header' }, { tag: tags.heading1, class: 'cm-header cm-header-1' }, { tag: tags.heading2, class: 'cm-header cm-header-2' }, { tag: tags.heading3, class: 'cm-header cm-header-3' }, { tag: tags.heading4, class: 'cm-header cm-header-4' }, { tag: tags.heading5, class: 'cm-header cm-header-5' }, { tag: tags.heading6, class: 'cm-header cm-header-6' }, { tag: tags.link, class: 'cm-link' }, { tag: tags.string, class: 'cm-string' }, { tag: tags.url, class: 'cm-string cm-url' }, { tag: tags.list, class: 'cm-list' }, { tag: tags.strong, class: 'cm-strong' }, { tag: tags.emphasis, class: 'cm-em' }, { tag: tags.quote, class: 'cm-quote' }, { tag: tags.comment, class: 'cm-comment' }, { tag: tags.monospace, class: 'cm-comment' }, //css tags { tag: tags.tagName, class: 'cm-tag' }, { tag: tags.className, class: 'cm-class' }, { tag: tags.propertyName, class: 'cm-property' }, { tag: tags.attributeValue, class: 'cm-value' }, { tag: tags.keyword, class: 'cm-keyword' }, { tag: tags.atom, class: 'cm-atom' }, { tag: tags.integer, class: 'cm-integer' }, { tag: tags.unit, class: 'cm-unit' }, { tag: tags.color, class: 'cm-color' }, { tag: tags.paren, class: 'cm-paren' }, { tag: tags.variableName, class: 'cm-variable' }, { tag: tags.invalid, class: 'cm-error' }, ]); function getUrl(node, doc) { let url = null; const cursor = node.node.cursor(); if(cursor.firstChild()) { do { if(cursor.name === 'URL') { url = doc.sliceString(cursor.from, cursor.to); break; } } while (cursor.nextSibling()); } return url; } export function customHighlightPlugin(renderer, tab) { //this function takes the custom tokens created in the tokenize function in customhighlight files //takes the tokens defined by that function and assigns classes to them //it also creates page number and snippet number widgets let tokenize; if(tab === 'brewStyles') { tokenize = tokenizeCustomCSS; } else { tokenize = renderer === 'V3' ? tokenizeCustomMarkdown : legacyTokenizeCustomMarkdown; } return ViewPlugin.fromClass( class { constructor(view) { this.decorations = this.buildDecorations(view); } update(update) { if(update.docChanged) { this.decorations = this.buildDecorations(update.view); } } buildDecorations(view) { const decos = []; const tokens = tokenize(view.state.doc.toString()); let pageCount = 1; let snippetCount = 0; const tree = ensureSyntaxTree(view.state, view.state.doc.length, 50) || syntaxTree(view.state); tree.iterate({ enter : (node)=>{ if(node.name === 'Image') { const url = getUrl(node, view.state.doc); if(!url) return; decos.push( Decoration.mark({ class : 'cm-image', attributes : { 'style' : `--preview-img:url(${url});` } }).range(node.from, node.to) ); } } }); tokens.forEach((token)=>{ const line = view.state.doc.line(token.line + 1); if(token.from != null && token.to != null && token.from < token.to) { const from = line.from + token.from; const to = line.from + token.to; const attrs = {}; if(token.type === 'Image' && token.url) { attrs['data-url'] = token.url; } decos.push( Decoration.mark({ class : `cm-${token.type}`, ...(Object.keys(attrs).length ? { attributes: attrs } : {}) }).range(from, to) ); } else { decos.push( Decoration.line({ class : `cm-${token.type}` }).range(line.from) ); if(token.type === 'pageLine' && tab === 'brewText') { pageCount++; if(line.from === 0) pageCount--; decos.push(Decoration.line({ attributes: { 'data-page-number': pageCount } }).range(line.from)); } if(token.type === 'snippetLine' && tab === 'brewSnippets') { snippetCount++; decos.push(Decoration.line({ attributes: { 'data-page-number': snippetCount } }).range(line.from)); } } }); decos.sort((a, b)=>a.from - b.from || a.to - b.to); return Decoration.set(decos); } }, { decorations: (v)=>v.decorations } ); };