mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-03-27 21:08:13 +00:00
organizing
This commit is contained in:
@@ -8,169 +8,19 @@ import {
|
|||||||
highlightActiveLineGutter,
|
highlightActiveLineGutter,
|
||||||
highlightActiveLine,
|
highlightActiveLine,
|
||||||
scrollPastEnd,
|
scrollPastEnd,
|
||||||
Decoration,
|
|
||||||
ViewPlugin,
|
|
||||||
WidgetType,
|
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { EditorState, Compartment } from '@codemirror/state';
|
import { EditorState, Compartment } from '@codemirror/state';
|
||||||
import { foldGutter, foldKeymap, syntaxHighlighting, HighlightStyle } from '@codemirror/language';
|
import { foldGutter, foldKeymap, syntaxHighlighting } from '@codemirror/language';
|
||||||
import { defaultKeymap, history, historyField, undo, redo } from '@codemirror/commands';
|
import { defaultKeymap, history, historyField, undo, redo } from '@codemirror/commands';
|
||||||
import { languages } from '@codemirror/language-data';
|
import { languages } from '@codemirror/language-data';
|
||||||
import { css } from '@codemirror/lang-css';
|
import { css } from '@codemirror/lang-css';
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
|
|
||||||
import { tags } from '@lezer/highlight';
|
|
||||||
|
|
||||||
// ######################### THEMES #############################
|
|
||||||
|
|
||||||
import * as themes from '@uiw/codemirror-themes-all';
|
import * as themes from '@uiw/codemirror-themes-all';
|
||||||
|
|
||||||
const themeCompartment = new Compartment();
|
const themeCompartment = new Compartment();
|
||||||
|
|
||||||
// ######################### CUSTOM HIGHLIGHTS #############################
|
import { customHighlightPlugin, customHighlightStyle } from './customHighlight.js';
|
||||||
|
import { homebreweryFold, hbFolding } from './customFolding.js';
|
||||||
const highlightStyle = HighlightStyle.define([
|
|
||||||
{
|
|
||||||
tag : tags.heading1,
|
|
||||||
color : 'black',
|
|
||||||
fontSize : '1.75em',
|
|
||||||
fontWeight : '700',
|
|
||||||
class : 'cm-header cm-header-1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tag : tags.processingInstruction,
|
|
||||||
color : 'blue',
|
|
||||||
},
|
|
||||||
// …
|
|
||||||
]);
|
|
||||||
|
|
||||||
import { tokenizeCustomMarkdown, customTags } from './customMarkdownGrammar.js';
|
|
||||||
|
|
||||||
const customHighlightStyle = HighlightStyle.define([
|
|
||||||
{ tag: tags.heading1, color: '#000', fontWeight: '700' },
|
|
||||||
{ tag: tags.keyword, color: '#07a' }, // example for your markdown headings
|
|
||||||
{ tag: customTags.pageLine, color: '#f0a' },
|
|
||||||
{ tag: customTags.snippetBreak, class: 'cm-snippet-break', color: '#0af' },
|
|
||||||
{ tag: customTags.inlineBlock, class: 'cm-inline-block', backgroundColor: '#fffae6' },
|
|
||||||
{ tag: customTags.emoji, class: 'cm-emoji', color: '#fa0' },
|
|
||||||
{ tag: customTags.superscript, class: 'cm-superscript', verticalAlign: 'super', fontSize: '0.8em' },
|
|
||||||
{ tag: customTags.subscript, class: 'cm-subscript', verticalAlign: 'sub', fontSize: '0.8em' },
|
|
||||||
{ tag: customTags.definitionTerm, class: 'cm-dt', fontWeight: 'bold', color: '#0a0' },
|
|
||||||
{ tag: customTags.definitionDesc, class: 'cm-dd', color: '#070' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const customHighlightPlugin = 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 = tokenizeCustomMarkdown(view.state.doc.toString());
|
|
||||||
|
|
||||||
tokens.forEach((tok)=>{
|
|
||||||
const line = view.state.doc.line(tok.line + 1);
|
|
||||||
|
|
||||||
if(tok.from != null && tok.to != null && tok.from < tok.to) {
|
|
||||||
// inline decoration
|
|
||||||
decos.push(
|
|
||||||
Decoration.mark({ class: `cm-${tok.type}` }).range(line.from + tok.from, line.from + tok.to),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// full-line decoration
|
|
||||||
decos.push(Decoration.line({ class: `cm-${tok.type}` }).range(line.from));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// sort by absolute start position
|
|
||||||
decos.sort((a, b)=>a.from - b.from || a.to - b.to);
|
|
||||||
|
|
||||||
return Decoration.set(decos);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
decorations : (v)=>v.decorations,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// ######################### FOLDING ###############################
|
|
||||||
|
|
||||||
import { foldService } from '@codemirror/language';
|
|
||||||
|
|
||||||
const homebreweryFold = foldService.of((state, lineStart)=>{
|
|
||||||
const doc = state.doc;
|
|
||||||
const matcher = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
|
||||||
|
|
||||||
const startLine = doc.lineAt(lineStart);
|
|
||||||
const prevLineText = startLine.number > 1 ? doc.line(startLine.number - 1).text : '';
|
|
||||||
|
|
||||||
if(startLine.number > 1 && !matcher.test(prevLineText)) return null;
|
|
||||||
|
|
||||||
let endLine = startLine.number;
|
|
||||||
while (endLine < doc.lines && !matcher.test(doc.line(endLine + 1).text)) {
|
|
||||||
endLine++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(endLine === startLine.number) return null;
|
|
||||||
|
|
||||||
const widgetObject = { from: startLine.from, to: doc.line(endLine).to };
|
|
||||||
console.log(widgetObject);
|
|
||||||
|
|
||||||
return widgetObject;
|
|
||||||
});
|
|
||||||
|
|
||||||
import { codeFolding } from '@codemirror/language';
|
|
||||||
|
|
||||||
function getFoldPreview(state, from, to) {
|
|
||||||
const doc = state.doc;
|
|
||||||
const startLine = doc.lineAt(from).number;
|
|
||||||
const endLine = doc.lineAt(to).number;
|
|
||||||
|
|
||||||
// If the current line has text, do not generate a preview
|
|
||||||
if (doc.line(startLine).text.trim().length > 0) {
|
|
||||||
return `↤ Lines ${startLine}-${endLine} ↦`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let preview = '';
|
|
||||||
|
|
||||||
for (let i = startLine + 1; i <= endLine; i++) {
|
|
||||||
const text = doc.line(i).text.trim();
|
|
||||||
if (text.length > 0) {
|
|
||||||
preview = text;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!preview) preview = `Lines ${startLine}-${endLine}`;
|
|
||||||
|
|
||||||
preview = preview.replace('{', '').trim();
|
|
||||||
if (preview.length > 50) preview = `${preview.slice(0, 50)}...`;
|
|
||||||
|
|
||||||
return `↤ ${preview} ↦`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hbFolding = codeFolding({
|
|
||||||
preparePlaceholder : (state, range)=>{
|
|
||||||
return getFoldPreview(state, range.from, range.to);
|
|
||||||
},
|
|
||||||
|
|
||||||
placeholderDOM(view, onclick, prepared) {
|
|
||||||
const span = document.createElement('span');
|
|
||||||
span.className = 'cm-fold-placeholder';
|
|
||||||
span.textContent = prepared;
|
|
||||||
span.onclick = onclick;
|
|
||||||
span.style.color = '#989898';
|
|
||||||
return span;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// ######################### COMPONENT #############################
|
|
||||||
|
|
||||||
const CodeEditor = forwardRef(
|
const CodeEditor = forwardRef(
|
||||||
(
|
(
|
||||||
@@ -242,8 +92,6 @@ const CodeEditor = forwardRef(
|
|||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
scrollPastEnd(),
|
scrollPastEnd(),
|
||||||
languageExtension,
|
languageExtension,
|
||||||
highlightActiveLine(),
|
|
||||||
highlightActiveLineGutter(),
|
|
||||||
|
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
homebreweryFold,
|
homebreweryFold,
|
||||||
@@ -254,9 +102,10 @@ const CodeEditor = forwardRef(
|
|||||||
openText : '▾',
|
openText : '▾',
|
||||||
closedText : '▸'
|
closedText : '▸'
|
||||||
}),
|
}),
|
||||||
themeCompartment.of(themeExtension), // 👈 key line
|
themeCompartment.of(themeExtension),
|
||||||
|
|
||||||
syntaxHighlighting(highlightStyle),
|
highlightActiveLine(),
|
||||||
|
highlightActiveLineGutter(),
|
||||||
customHighlightPlugin,
|
customHighlightPlugin,
|
||||||
syntaxHighlighting(customHighlightStyle),
|
syntaxHighlighting(customHighlightStyle),
|
||||||
];
|
];
|
||||||
@@ -265,7 +114,6 @@ const CodeEditor = forwardRef(
|
|||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(!editorRef.current) return;
|
if(!editorRef.current) return;
|
||||||
|
|
||||||
// create initial editor state
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc : value,
|
doc : value,
|
||||||
extensions : createExtensions({ onChange, language, editorTheme }),
|
extensions : createExtensions({ onChange, language, editorTheme }),
|
||||||
@@ -276,7 +124,6 @@ const CodeEditor = forwardRef(
|
|||||||
parent : editorRef.current,
|
parent : editorRef.current,
|
||||||
});
|
});
|
||||||
|
|
||||||
// save initial state for current tab
|
|
||||||
docsRef.current[tab] = state;
|
docsRef.current[tab] = state;
|
||||||
|
|
||||||
return ()=>viewRef.current?.destroy();
|
return ()=>viewRef.current?.destroy();
|
||||||
@@ -289,10 +136,8 @@ const CodeEditor = forwardRef(
|
|||||||
const prevTab = prevTabRef.current;
|
const prevTab = prevTabRef.current;
|
||||||
|
|
||||||
if(prevTab !== tab) {
|
if(prevTab !== tab) {
|
||||||
// save current state
|
|
||||||
docsRef.current[prevTab] = view.state;
|
docsRef.current[prevTab] = view.state;
|
||||||
|
|
||||||
// restore or create
|
|
||||||
let nextState = docsRef.current[tab];
|
let nextState = docsRef.current[tab];
|
||||||
|
|
||||||
if(!nextState) {
|
if(!nextState) {
|
||||||
|
|||||||
51
client/components/codeEditor/customFolding.js
Normal file
51
client/components/codeEditor/customFolding.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { foldService } from '@codemirror/language';
|
||||||
|
import { codeFolding } from '@codemirror/language';
|
||||||
|
|
||||||
|
export function getFoldPreview(state, from, to) {
|
||||||
|
const doc = state.doc;
|
||||||
|
const start = doc.lineAt(from).number;
|
||||||
|
const end = doc.lineAt(to).number;
|
||||||
|
|
||||||
|
if(doc.line(start).text.trim()) return ` ↤ Lines ${start}-${end} ↦`;
|
||||||
|
|
||||||
|
const preview = Array.from({ length: end - start }, (_, i)=>doc.line(start + 1 + i).text.trim())
|
||||||
|
.find(Boolean) || `Lines ${start}-${end}`;
|
||||||
|
|
||||||
|
return ` ↤ ${preview.replace('{', '').slice(0, 50).trim()}${preview.length > 50 ? '...' : ''} ↦`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const homebreweryFold = foldService.of((state, lineStart)=>{
|
||||||
|
const doc = state.doc;
|
||||||
|
const matcher = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||||
|
|
||||||
|
const startLine = doc.lineAt(lineStart);
|
||||||
|
const prevLineText = startLine.number > 1 ? doc.line(startLine.number - 1).text : '';
|
||||||
|
|
||||||
|
if(startLine.number > 1 && !matcher.test(prevLineText)) return null;
|
||||||
|
|
||||||
|
let endLine = startLine.number;
|
||||||
|
while (endLine < doc.lines && !matcher.test(doc.line(endLine + 1).text)) {
|
||||||
|
endLine++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(endLine === startLine.number) return null;
|
||||||
|
|
||||||
|
const widgetObject = { from: startLine.from, to: doc.line(endLine).to };
|
||||||
|
console.log(widgetObject);
|
||||||
|
|
||||||
|
return widgetObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const hbFolding = codeFolding({
|
||||||
|
preparePlaceholder : (state, range)=>{
|
||||||
|
return getFoldPreview(state, range.from, range.to);
|
||||||
|
},
|
||||||
|
placeholderDOM(view, onclick, prepared) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = 'cm-fold-placeholder';
|
||||||
|
span.textContent = prepared;
|
||||||
|
span.onclick = onclick;
|
||||||
|
span.style.color = '#989898';
|
||||||
|
return span;
|
||||||
|
},
|
||||||
|
});
|
||||||
283
client/components/codeEditor/customHighlight.js
Normal file
283
client/components/codeEditor/customHighlight.js
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import { HighlightStyle } from '@codemirror/language';
|
||||||
|
import { tags } from '@lezer/highlight';
|
||||||
|
import {
|
||||||
|
Decoration,
|
||||||
|
ViewPlugin,
|
||||||
|
} from '@codemirror/view';
|
||||||
|
|
||||||
|
// Making the tokens
|
||||||
|
const customTags = {
|
||||||
|
pageLine : 'pageLine', // .cm-pageLine
|
||||||
|
snippetLine : 'snippetLine', // .cm-snippetLine
|
||||||
|
columnSplit : 'columnSplit', // .cm-columnSplit
|
||||||
|
snippetBreak : 'snippetBreak', // .cm-snippetBreak
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
function tokenizeCustomMarkdown(text) {
|
||||||
|
const tokens = [];
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
// Track multi-line blocks
|
||||||
|
const inBlock = false;
|
||||||
|
const blockStart = 0;
|
||||||
|
|
||||||
|
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 });
|
||||||
|
if(/\\snippet/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.snippetBreak });
|
||||||
|
|
||||||
|
// --- Emoji ---
|
||||||
|
if(/:\w+?:/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.emoji });
|
||||||
|
|
||||||
|
// --- 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- inline definition lists ---
|
||||||
|
if(/::/.test(lineText)) {
|
||||||
|
if(/^:*$/.test(lineText) == true) {
|
||||||
|
return; //if line only has colons, stops
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleLineRegex = /^([^:\n]*\S)(::)([^\n]*)$/dmy;
|
||||||
|
|
||||||
|
const match = singleLineRegex.exec(lineText);
|
||||||
|
|
||||||
|
if(match) {
|
||||||
|
const [full, term, colons, desc] = match;
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
// Entire line as definitionList
|
||||||
|
tokens.push({
|
||||||
|
line : lineNumber,
|
||||||
|
type : customTags.definitionList,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Term
|
||||||
|
tokens.push({
|
||||||
|
line : lineNumber,
|
||||||
|
type : customTags.definitionTerm,
|
||||||
|
from : offset,
|
||||||
|
to : offset + term.length,
|
||||||
|
});
|
||||||
|
offset += term.length;
|
||||||
|
|
||||||
|
// ::
|
||||||
|
tokens.push({
|
||||||
|
line : lineNumber,
|
||||||
|
type : customTags.definitionColon,
|
||||||
|
from : offset,
|
||||||
|
to : offset + colons.length,
|
||||||
|
});
|
||||||
|
offset += colons.length;
|
||||||
|
|
||||||
|
// Definition
|
||||||
|
tokens.push({
|
||||||
|
line : lineNumber,
|
||||||
|
type : customTags.definitionDesc,
|
||||||
|
from : offset,
|
||||||
|
to : offset + desc.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiline def list
|
||||||
|
if(!/^::/.test(lines[lineNumber]) && lineNumber + 1 < lines.length && /^::/.test(lines[lineNumber + 1])) {
|
||||||
|
const term = lineText;
|
||||||
|
const startLine = lineNumber;
|
||||||
|
const defs = [];
|
||||||
|
let endLine = startLine;
|
||||||
|
|
||||||
|
// collect all following :: definitions
|
||||||
|
for (let i = lineNumber + 1; i < lines.length; i++) {
|
||||||
|
const nextLine = lines[i];
|
||||||
|
const onlyColonsMatch = /^:*$/.test(nextLine);
|
||||||
|
const defMatch = /^(::)(.*\S.*)?\s*$/.exec(nextLine);
|
||||||
|
if(!onlyColonsMatch && defMatch) {
|
||||||
|
defs.push({ colons: defMatch[1], desc: defMatch[2], line: i });
|
||||||
|
endLine = i;
|
||||||
|
} else break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(defs.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})/gm;
|
||||||
|
let match;
|
||||||
|
while ((match = injectionRegex.exec(lineText)) !== null) {
|
||||||
|
tokens.push({
|
||||||
|
line : lineNumber,
|
||||||
|
from : match.index +1,
|
||||||
|
to : match.index + match[1].length +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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const customHighlightStyle = HighlightStyle.define([
|
||||||
|
{ tag: tags.heading1, color: '#000', fontWeight: '700' },
|
||||||
|
{ tag: tags.keyword, color: '#07a' }, // example for your markdown headings
|
||||||
|
{ tag: customTags.pageLine, color: '#f0a' },
|
||||||
|
{ tag: customTags.snippetBreak, class: 'cm-snippet-break', color: '#0af' },
|
||||||
|
{ tag: customTags.inlineBlock, class: 'cm-inline-block', backgroundColor: '#fffae6' },
|
||||||
|
{ tag: customTags.emoji, class: 'cm-emoji', color: '#fa0' },
|
||||||
|
{ tag: customTags.superscript, class: 'cm-superscript', verticalAlign: 'super', fontSize: '0.8em' },
|
||||||
|
{ tag: customTags.subscript, class: 'cm-subscript', verticalAlign: 'sub', fontSize: '0.8em' },
|
||||||
|
{ tag: customTags.definitionTerm, class: 'cm-dt', fontWeight: 'bold', color: '#0a0' },
|
||||||
|
{ tag: customTags.definitionDesc, class: 'cm-dd', color: '#070' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const customHighlightPlugin = 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 = tokenizeCustomMarkdown(view.state.doc.toString());
|
||||||
|
|
||||||
|
tokens.forEach((tok)=>{
|
||||||
|
const line = view.state.doc.line(tok.line + 1);
|
||||||
|
|
||||||
|
if(tok.from != null && tok.to != null && tok.from < tok.to) {
|
||||||
|
// inline decoration
|
||||||
|
decos.push(
|
||||||
|
Decoration.mark({ class: `cm-${tok.type}` }).range(line.from + tok.from, line.from + tok.to),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// full-line decoration
|
||||||
|
decos.push(Decoration.line({ class: `cm-${tok.type}` }).range(line.from));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort by absolute start position
|
||||||
|
decos.sort((a, b)=>a.from - b.from || a.to - b.to);
|
||||||
|
|
||||||
|
return Decoration.set(decos);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations : (v)=>v.decorations,
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
// customMarkdownGrammar.js
|
|
||||||
|
|
||||||
// --- Custom tags with CM6-compatible class names ---
|
|
||||||
export const customTags = {
|
|
||||||
pageLine: "pageLine", // .cm-pageLine
|
|
||||||
snippetLine: "snippetLine", // .cm-snippetLine
|
|
||||||
columnSplit: "columnSplit", // .cm-columnSplit
|
|
||||||
snippetBreak: "snippetBreak", // .cm-snippetBreak
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Tokenizer function ---
|
|
||||||
export function tokenizeCustomMarkdown(text) {
|
|
||||||
const tokens = [];
|
|
||||||
const lines = text.split("\n");
|
|
||||||
|
|
||||||
// Track multi-line blocks
|
|
||||||
let inBlock = false;
|
|
||||||
let blockStart = 0;
|
|
||||||
|
|
||||||
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 });
|
|
||||||
if (/\\snippet/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.snippetBreak });
|
|
||||||
|
|
||||||
// --- Emoji ---
|
|
||||||
if (/:\w+?:/.test(lineText)) tokens.push({ line: lineNumber, type: customTags.emoji });
|
|
||||||
|
|
||||||
// --- 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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- inline definition lists ---
|
|
||||||
if (/::/.test(lineText)) {
|
|
||||||
if (/^:*$/.test(lineText) == true) {
|
|
||||||
return; //if line only has colons, stops
|
|
||||||
}
|
|
||||||
|
|
||||||
const singleLineRegex = /^([^:\n]*\S)(::)([^\n]*)$/dmy;
|
|
||||||
|
|
||||||
let match = singleLineRegex.exec(lineText);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [full, term, colons, desc] = match;
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
// Entire line as definitionList
|
|
||||||
tokens.push({
|
|
||||||
line: lineNumber,
|
|
||||||
type: customTags.definitionList,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Term
|
|
||||||
tokens.push({
|
|
||||||
line: lineNumber,
|
|
||||||
type: customTags.definitionTerm,
|
|
||||||
from: offset,
|
|
||||||
to: offset + term.length,
|
|
||||||
});
|
|
||||||
offset += term.length;
|
|
||||||
|
|
||||||
// ::
|
|
||||||
tokens.push({
|
|
||||||
line: lineNumber,
|
|
||||||
type: customTags.definitionColon,
|
|
||||||
from: offset,
|
|
||||||
to: offset + colons.length,
|
|
||||||
});
|
|
||||||
offset += colons.length;
|
|
||||||
|
|
||||||
// Definition
|
|
||||||
tokens.push({
|
|
||||||
line: lineNumber,
|
|
||||||
type: customTags.definitionDesc,
|
|
||||||
from: offset,
|
|
||||||
to: offset + desc.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// multiline def list
|
|
||||||
if (!/^::/.test(lines[lineNumber]) && lineNumber + 1 < lines.length && /^::/.test(lines[lineNumber + 1])) {
|
|
||||||
const term = lineText;
|
|
||||||
const startLine = lineNumber;
|
|
||||||
let defs = [];
|
|
||||||
let endLine = startLine;
|
|
||||||
|
|
||||||
// collect all following :: definitions
|
|
||||||
for (let i = lineNumber + 1; i < lines.length; i++) {
|
|
||||||
const nextLine = lines[i];
|
|
||||||
const onlyColonsMatch = /^:*$/.test(nextLine);
|
|
||||||
const defMatch = /^(::)(.*\S.*)?\s*$/.exec(nextLine);
|
|
||||||
if (!onlyColonsMatch && defMatch) {
|
|
||||||
defs.push({ colons: defMatch[1], desc: defMatch[2], line: i });
|
|
||||||
endLine = i;
|
|
||||||
} else break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defs.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})/gm;
|
|
||||||
let match;
|
|
||||||
while ((match = injectionRegex.exec(lineText)) !== null) {
|
|
||||||
tokens.push({
|
|
||||||
line: lineNumber,
|
|
||||||
from: match.index +1,
|
|
||||||
to: match.index + match[1].length +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;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
export default {
|
|
||||||
registerHomebreweryHelper : function(CodeMirror) {
|
|
||||||
CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) {
|
|
||||||
const matcher = /^\\page.*/;
|
|
||||||
const prevLine = cm.getLine(start.line - 1);
|
|
||||||
|
|
||||||
if(start.line === cm.firstLine() || prevLine.match(matcher)) {
|
|
||||||
const lastLineNo = cm.lastLine();
|
|
||||||
let end = start.line;
|
|
||||||
|
|
||||||
while (end < lastLineNo) {
|
|
||||||
if(cm.getLine(end + 1).match(matcher))
|
|
||||||
break;
|
|
||||||
++end;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
from : CodeMirror.Pos(start.line, 0),
|
|
||||||
to : CodeMirror.Pos(end, cm.getLine(end).length)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user