mirror of
https://github.com/utleysam/codeblock-crbasic.git
synced 2026-07-22 07:32:57 +00:00
Initial release v1.0.0
This commit is contained in:
parent
f3b0208c13
commit
a6f9fac67c
9 changed files with 584 additions and 0 deletions
37
esbuild.config.mjs
Normal file
37
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
|
||||
const watch = process.argv.includes("--watch");
|
||||
|
||||
const context = await esbuild.context({
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
format: "cjs",
|
||||
target: "es2020",
|
||||
platform: "browser",
|
||||
sourcemap: false,
|
||||
outfile: "main.js",
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
],
|
||||
logLevel: "info",
|
||||
});
|
||||
|
||||
if (watch) {
|
||||
await context.watch();
|
||||
} else {
|
||||
await context.rebuild();
|
||||
await context.dispose();
|
||||
}
|
||||
10
manifest.json
Normal file
10
manifest.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"id": "codeblock-crbasic",
|
||||
"name": "CodeBlock CRBasic",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "1.12.7",
|
||||
"description": "Syntax highlighting for CRBasic (Campbell Scientific) markdown code blocks.",
|
||||
"author": "samutley",
|
||||
"authorUrl": "https://github.com/utleysam",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
19
package.json
Normal file
19
package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "codeblock-crbasic",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Obsidian plugin for CRBasic syntax highlighting in code blocks",
|
||||
"scripts": {
|
||||
"build": "node esbuild.config.mjs",
|
||||
"dev": "node esbuild.config.mjs --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"esbuild": "^0.25.0",
|
||||
"obsidian": "latest",
|
||||
"typescript": "^5.7.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0"
|
||||
}
|
||||
}
|
||||
90
src/cm6-crbasic.ts
Normal file
90
src/cm6-crbasic.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* CodeMirror 6 ViewPlugin for CRBasic syntax highlighting in code blocks.
|
||||
*
|
||||
* Obsidian uses HyperMD which has different syntax tree node names than
|
||||
* standard CM6 markdown. Instead of depending on those names, this plugin
|
||||
* scans the document text for ```crbasic fences and tokenizes the content.
|
||||
*/
|
||||
|
||||
import {
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
EditorView,
|
||||
} from "@codemirror/view";
|
||||
import { RangeSetBuilder } from "@codemirror/state";
|
||||
import { tokenizeLine, cmClass, TokenType } from "./tokenizer";
|
||||
|
||||
// ── Pre-built decoration cache ──────────────────────────────────────
|
||||
const decoCache: Record<string, Decoration> = {};
|
||||
function getDeco(type: TokenType): Decoration {
|
||||
const cls = cmClass(type);
|
||||
if (!decoCache[cls]) {
|
||||
decoCache[cls] = Decoration.mark({ class: cls });
|
||||
}
|
||||
return decoCache[cls];
|
||||
}
|
||||
|
||||
// ── ViewPlugin ──────────────────────────────────────────────────────
|
||||
|
||||
class CRBasicHighlighter {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView): DecorationSet {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
const doc = view.state.doc;
|
||||
|
||||
// Scan document lines for ```crbasic blocks
|
||||
let inCrbasicBlock = false;
|
||||
const contentLines: { from: number; text: string }[] = [];
|
||||
|
||||
for (let i = 1; i <= doc.lines; i++) {
|
||||
const line = doc.line(i);
|
||||
const trimmed = line.text.trim().toLowerCase();
|
||||
|
||||
if (!inCrbasicBlock) {
|
||||
// Check for opening fence: ```crbasic (with optional trailing whitespace)
|
||||
if (/^```crbasic\s*$/i.test(line.text.trim())) {
|
||||
inCrbasicBlock = true;
|
||||
contentLines.length = 0;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Check for closing fence
|
||||
if (/^```\s*$/.test(line.text.trim())) {
|
||||
// Tokenize all collected content lines and add decorations
|
||||
for (const cl of contentLines) {
|
||||
const tokens = tokenizeLine(cl.text, cl.from);
|
||||
for (const tok of tokens) {
|
||||
if (tok.from < tok.to) {
|
||||
builder.add(tok.from, tok.to, getDeco(tok.type));
|
||||
}
|
||||
}
|
||||
}
|
||||
inCrbasicBlock = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect content line
|
||||
contentLines.push({ from: line.from, text: line.text });
|
||||
}
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
}
|
||||
|
||||
export const crbasicHighlighter = ViewPlugin.fromClass(CRBasicHighlighter, {
|
||||
decorations: (v) => v.decorations,
|
||||
});
|
||||
194
src/crbasic-tokens.ts
Normal file
194
src/crbasic-tokens.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
/**
|
||||
* Shared CRBasic token lists — single source of truth for both
|
||||
* CodeMirror 6 and highlight.js highlighters.
|
||||
*
|
||||
* Ported from the VSCode CRBasic extension's TextMate grammar
|
||||
* (daiwalkr.cr-basic-ms-vscode, v2026.5.1).
|
||||
*
|
||||
* CRBasic is case-insensitive; all matching should use the "i" flag.
|
||||
*/
|
||||
|
||||
// ── Keywords (keyword.control) ──────────────────────────────────────
|
||||
export const KEYWORDS = [
|
||||
"Alias", "ArrayIndex", "BeginProg", "EndProg", "Call", "CallTable",
|
||||
"Case", "Is", "Const", "ConstTable", "EndConstTable", "ContinueScan",
|
||||
"Delay", "Dim", "Do", "Else", "ElseIf", "Then", "EndIf",
|
||||
"EndSequence", "ESSInitialize", "EssVariables", "Exit", "ExitDo",
|
||||
"ExitFor", "ExitFunction", "ExitScan", "ExitSub", "FileManage", "To",
|
||||
"Function", "EndFunction", "If", "IfTime", "IIF", "Include",
|
||||
"IncludeSection", "Loop", "NewFieldNames", "Optional", "Public",
|
||||
"ReadOnly", "Scan", "NextScan", "Select", "EndSelect",
|
||||
"SemaphoreGet", "SemaphoreRelease", "SlowSequence", "Step", "Sub",
|
||||
"EndSub", "SubScan", "NextSubScan", "TriggerSequence", "Units",
|
||||
"Until", "WaitDigTrig", "WaitTriggerSequence", "While", "Wend",
|
||||
"For", "Next",
|
||||
];
|
||||
|
||||
// ── Preprocessor directives (keyword.control.preprocessor) ──────────
|
||||
// Matched WITH the leading "#"
|
||||
export const PREPROCESSOR = [
|
||||
"#If", "#Else", "#ElseIf", "#EndIf", "#IfDev", "#If_no_remove", "#UnDef",
|
||||
];
|
||||
|
||||
// ── Logical operators (keyword.operator.logical) ────────────────────
|
||||
export const LOGICAL_OPERATORS = [
|
||||
"AND", "IMP", "INTDV", "MOD", "NOT", "OR", "XOR",
|
||||
];
|
||||
|
||||
// ── Constants (constant.character + constant.language) ───────────────
|
||||
export const CONSTANTS = [
|
||||
"As", "True", "False",
|
||||
"LoggerType", "LoggerEndian", "CR_LITTLE_ENDIAN", "CR_BIG_ENDIAN",
|
||||
];
|
||||
|
||||
// ── Built-in functions (entity.name.function.*) ─────────────────────
|
||||
// Each sub-array corresponds to one TextMate category.
|
||||
|
||||
const CDM = [
|
||||
"CDM_ACPower", "CDM_Battery", "CDM_BrFull", "CDM_BrFull6W",
|
||||
"CDM_BrHalf", "CDM_BrHalf3W", "CDM_BrHalf4W", "CDM_BridgeFilt",
|
||||
"CDM_Delay", "CDM_ExciteI", "CDM_ExciteV", "CDM_FFTFilt",
|
||||
"CDM_MuxSelect", "CDM_PanelTemp", "CDM_PeriodAvg", "CDM_PulsePort",
|
||||
"CDM_Resistance", "CDM_Resistance3W", "CDM_SW12", "CDM_SW5",
|
||||
"CDM_TCComp", "CDM_TCDiff", "CDM_TCSE", "CDM_Therm107",
|
||||
"CDM_Therm108", "CDM_Therm109", "CDM_VoltDiff", "CDM_VoltFilt",
|
||||
"CDM_VoltSE", "CDM_VW300Config", "CDM_VW300Dynamic",
|
||||
"CDM_VW300Rainflow", "CDM_VW300Static", "CPIAddModule",
|
||||
"CPIFileSend", "CPISpeed", "RainFlowSample",
|
||||
];
|
||||
|
||||
const CUSTOM_MENUS = [
|
||||
"DisplayLine", "DisplayMenu", "EndMenu", "DisplayValue", "MenuItem",
|
||||
"MenuPick", "MenuRecompile", "SubMenu", "EndSubMenu",
|
||||
];
|
||||
|
||||
const DATA_TABLE = [
|
||||
"Average", "CardFlush", "CardOut", "DataEvent",
|
||||
"DataInterval", "DataTable", "EndTable", "DataTime", "ETsz",
|
||||
"FFTSample", "FieldClassify", "FieldNames", "FieldOrigin",
|
||||
"FileMark", "FillStop", "GOESAppend", "GOESField", "GoesTable",
|
||||
"Histogram", "Histogram4D", "LevelCrossing", "Maximum", "Median",
|
||||
"Minimum", "Moment", "OpenInterval", "ResetTable", "Sample",
|
||||
"SampleFieldCal", "SampleMaxMin", "StdDev", "StructureType",
|
||||
"EndStructureType", "TableFieldNames", "TableFile", "TableHide",
|
||||
"Totalize", "WindVector", "WorstCase",
|
||||
];
|
||||
|
||||
const DATALOGGER_STATUS = [
|
||||
"ApplyandRestartSequence", "EndApplyandRestartSequence", "ArrayLength",
|
||||
"CalFile", "Calibrate", "CheckPort", "ClockChange", "ClockSet",
|
||||
"ComPortIsActive", "Data", "DataLong", "DaylightSaving",
|
||||
"DaylightSavingUS", "Debug", "DebugBreak", "Encryption", "Erase",
|
||||
"EthernetPower", "FieldCal", "FieldCalStrain", "GetRecord", "GPS",
|
||||
"InstructionTimes", "LineNum", "LoadFieldCal", "MonitorComms",
|
||||
"Move", "MuxSelect", "NewFieldCal", "NewFile", "PipeLineMode",
|
||||
"PortBridge", "PortGet", "PortPairConfig", "PortsConfig", "PortSet",
|
||||
"PreserveVariables", "PulsePort", "PWM", "Read", "ReadIO",
|
||||
"RealTime", "Restart", "Restore", "SDI12SensorSetup",
|
||||
"SDI12SensorResponse", "SecsSince1990", "SequentialMode",
|
||||
"SetSecurity", "SetSetting", "SetStatus", "ShutDownBegin",
|
||||
"ShutDownEnd", "Signature", "StationName", "SW12", "SWVX",
|
||||
"TimeIntoInterval", "TimeIsBetween", "Timer", "WatchdogTimer",
|
||||
"WriteIO",
|
||||
];
|
||||
|
||||
const FILE_IO = [
|
||||
"FileClose", "FileCopy", "FileEncrypt", "FileList", "FileManage",
|
||||
"FileOpen", "FileRead", "FileReadLine", "FileRename", "FileSize",
|
||||
"FileTime", "FileWrite", "GetFile", "GZip", "NewFile", "SendFile",
|
||||
];
|
||||
|
||||
const INTERNET = [
|
||||
"DHCPRenew", "EmailRecv", "EmailRelay", "EmailSend", "EthernetPower",
|
||||
"FTPClient", "HTTPGet", "HTTPOut", "HTTPPost", "HTTPPut", "IPInfo",
|
||||
"IPNetPower", "IPRoute", "IPTrace", "MQTTConnect", "MQTTPublish",
|
||||
"MQTTPublishConstTable", "MQTTPublishTable", "NetworkTimeProtocol",
|
||||
"PingIP", "PPPClose", "PPPOpen", "SMSRecv", "SMSSend",
|
||||
"SNMPVariable", "TCPActiveConnections", "TCPClose", "TCPOpen",
|
||||
"DNSQuery", "UDPDataGram", "UDPOpen", "UDPSocketClose",
|
||||
"UDPSocketOpen", "UDPSocketRecv", "UDPSocketSend", "WebPageBegin",
|
||||
"WebPageEnd", "XMLParse",
|
||||
];
|
||||
|
||||
const MATH = [
|
||||
"ABS", "ACOS", "AddPrecise", "AngleDegrees", "ASCII", "ASIN",
|
||||
"ATN", "ATN2", "AvgRun", "AvgSpa", "Ceiling", "CheckSum", "COS",
|
||||
"COSH", "Covariance", "CovSpa", "CTYPE", "DewPoint", "EXP", "FFT",
|
||||
"FFTSpa", "FindSpa", "FIX", "Floor", "FRAC", "Hex", "HexToDec",
|
||||
"INT", "LN", "LOG", "LOG10", "Matrix", "MaxRun", "MaxSpa",
|
||||
"MinRun", "MinSpa", "MoveBytes", "MovePrecise", "PeakValley", "PRT",
|
||||
"PRTCalc", "PWR", "Rainflow", "Randomize", "RectPolar", "RMSSpa",
|
||||
"RND", "Round", "SatVP", "SGN", "SIN", "SINH", "SolarPosition",
|
||||
"SortSpa", "SortSpaIndexed", "SQR", "StdDevRun", "StdDevSpa",
|
||||
"StrainCalc", "TAN", "TANH", "TotalRun", "VaporPressure",
|
||||
"WetDryBulb",
|
||||
];
|
||||
|
||||
const MEASUREMENTS = [
|
||||
"ACPower", "AM25T", "AVW200", "Battery", "BrFull", "BrFull6W",
|
||||
"BrHalf", "BrHalf3W", "BrHalf4W", "CS616", "CS7500", "CSAT3",
|
||||
"CSAT3B", "CSAT3BMonitor", "CurrentSE", "CWB100",
|
||||
"CWB100Diagnostics", "CWB100Routes", "CWB100RSSI", "EC100",
|
||||
"EC100Configure", "ExciteI", "ExciteV", "HydraProbe", "LI7200",
|
||||
"LI7700", "PanelTemp", "PeriodAvg", "PulseCount", "PulseCountReset",
|
||||
"Quadrature", "Resistance", "Resistance3W", "SDI12Recorder",
|
||||
"TCDiff", "TCSE", "TDR100", "TDR200", "TGA", "Therm107",
|
||||
"Therm108", "Therm109", "TimerInput", "VoltDiff", "VoltSE",
|
||||
];
|
||||
|
||||
const PAKBUS = [
|
||||
"AcceptDataRecords", "Broadcast", "ClockReport", "DataGram",
|
||||
"EncryptExempt", "GetDataRecord", "GetFile", "GetVariables",
|
||||
"NetWork", "PakBusCLock", "Route", "RouteNeighbors", "Routes",
|
||||
"SendData", "SendFile", "SendGetVariables", "SendTableDef",
|
||||
"SendVariables", "StaticRoute", "TimeUntilTransmit",
|
||||
];
|
||||
|
||||
const SDM = [
|
||||
"SDMAO4", "SDMAO4A", "SDMBeginPort", "SDMCAN", "SDMCD16AC",
|
||||
"SDMCD16Mask", "SDMCVO4", "SDMGeneric", "SDMINT8", "SDMIO16",
|
||||
"SDMSIO2R", "SDMSIO4", "SDMSpeed", "SDMSW8A", "SDMTrigger",
|
||||
"SDMX50",
|
||||
];
|
||||
|
||||
const SPECIAL_COMM = [
|
||||
"ArgosData", "ArgosDataRepeat", "ArgosError", "ArgosSetup",
|
||||
"ArgosTransmit", "DialModem", "DialSequence", "DialEndSequence",
|
||||
"DNP", "DNPUpdate", "DNPVariable", "GOESData", "GOESGPS",
|
||||
"GOESSetup", "GOESStatus", "GPS", "I2COpen", "I2CRead", "I2CWrite",
|
||||
"ModBusClient", "ModBusServer", "ModemCallBack", "ModemHangup",
|
||||
"EndModemHangup", "SPIOpen", "SPIRead", "SPIWrite",
|
||||
];
|
||||
|
||||
const SERIAL_IO = [
|
||||
"CheckSum", "ComPortIsActive", "MoveBytes", "SerialBrk",
|
||||
"SerialClose", "SerialFlush", "SerialIn", "SerialInBlock",
|
||||
"SerialInChk", "SerialInRecord", "SerialOpen", "SerialOut",
|
||||
"SerialOutBlock",
|
||||
];
|
||||
|
||||
const STRING_MANIP = [
|
||||
"ASCII", "Base64Encode", "CHR", "FormatFloat", "FormatLong",
|
||||
"FormatLongLong", "Hex", "HexToDec", "InStr", "Left", "Len",
|
||||
"LowerCase", "LTrim", "Mid", "Replace", "Right", "RTrim",
|
||||
"SplitStr", "Sprintf", "StrComp", "Trim", "TypeOf", "UpperCase",
|
||||
];
|
||||
|
||||
// Deduplicated flat list of all built-in functions
|
||||
export const FUNCTIONS: string[] = Array.from(new Set([
|
||||
...CDM, ...CUSTOM_MENUS, ...DATA_TABLE, ...DATALOGGER_STATUS,
|
||||
...FILE_IO, ...INTERNET, ...MATH, ...MEASUREMENTS, ...PAKBUS,
|
||||
...SDM, ...SPECIAL_COMM, ...SERIAL_IO, ...STRING_MANIP,
|
||||
]));
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Build a case-insensitive word-boundary regex from a list of words. */
|
||||
export function wordRegex(words: string[]): RegExp {
|
||||
return new RegExp("\\b(" + words.join("|") + ")\\b", "i");
|
||||
}
|
||||
|
||||
/** Build a Set of lower-cased words for fast O(1) lookup. */
|
||||
export function wordSet(words: string[]): Set<string> {
|
||||
return new Set(words.map((w) => w.toLowerCase()));
|
||||
}
|
||||
78
src/main.ts
Normal file
78
src/main.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* CodeBlock CRBasic — Obsidian plugin
|
||||
*
|
||||
* Adds CRBasic syntax highlighting to markdown fenced code blocks
|
||||
* in both Edit mode (CodeMirror 6 ViewPlugin) and Reading view
|
||||
* (registerMarkdownCodeBlockProcessor).
|
||||
*/
|
||||
|
||||
import { Plugin } from "obsidian";
|
||||
import { crbasicHighlighter } from "./cm6-crbasic";
|
||||
import { tokenizeLine, TokenType } from "./tokenizer";
|
||||
|
||||
/** Map token types to PrismJS CSS class arrays for reading view.
|
||||
* PrismJS tokens need both "token" and the type class. */
|
||||
const PRISM_CLASSES: Record<TokenType, string[]> = {
|
||||
keyword: ["token", "keyword"],
|
||||
builtin: ["token", "function"],
|
||||
operator: ["token", "operator"],
|
||||
atom: ["token", "boolean"],
|
||||
number: ["token", "number"],
|
||||
string: ["token", "string"],
|
||||
comment: ["token", "comment"],
|
||||
meta: ["token", "keyword"],
|
||||
};
|
||||
|
||||
export default class CodeBlockCRBasicPlugin extends Plugin {
|
||||
async onload() {
|
||||
// ── Edit mode (Live Preview): CM6 ViewPlugin ────────────────
|
||||
this.registerEditorExtension(crbasicHighlighter);
|
||||
|
||||
// ── Reading view: code block processor ──────────────────────
|
||||
this.registerMarkdownCodeBlockProcessor("crbasic", (source, el) => {
|
||||
// Match Obsidian's expected DOM structure for styled code blocks
|
||||
const pre = el.createEl("pre", { cls: ["language-crbasic"] });
|
||||
const code = pre.createEl("code", { cls: ["language-crbasic", "is-loaded"] });
|
||||
|
||||
const lines = source.split("\n");
|
||||
for (let li = 0; li < lines.length; li++) {
|
||||
const lineText = lines[li];
|
||||
const tokens = tokenizeLine(lineText, 0);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
// No tokens — plain text line
|
||||
code.appendText(lineText);
|
||||
} else {
|
||||
// Build the line with highlighted spans
|
||||
let cursor = 0;
|
||||
for (const tok of tokens) {
|
||||
// Text before this token
|
||||
if (tok.from > cursor) {
|
||||
code.appendText(lineText.slice(cursor, tok.from));
|
||||
}
|
||||
// Token span with PrismJS-compatible classes
|
||||
code.createEl("span", {
|
||||
cls: PRISM_CLASSES[tok.type],
|
||||
text: lineText.slice(tok.from, tok.to),
|
||||
});
|
||||
cursor = tok.to;
|
||||
}
|
||||
// Remaining text after last token
|
||||
if (cursor < lineText.length) {
|
||||
code.appendText(lineText.slice(cursor));
|
||||
}
|
||||
}
|
||||
|
||||
// Add newline between lines (not after the last line)
|
||||
if (li < lines.length - 1) {
|
||||
code.appendText("\n");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// CM6 extensions and markdown processors are automatically
|
||||
// cleaned up by Obsidian's plugin lifecycle.
|
||||
}
|
||||
}
|
||||
136
src/tokenizer.ts
Normal file
136
src/tokenizer.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* Standalone CRBasic tokenizer — shared by both CM6 and Reading View.
|
||||
* Returns token ranges with CSS class names for decoration.
|
||||
*/
|
||||
|
||||
import {
|
||||
KEYWORDS,
|
||||
LOGICAL_OPERATORS,
|
||||
CONSTANTS,
|
||||
FUNCTIONS,
|
||||
PREPROCESSOR,
|
||||
wordSet,
|
||||
} from "./crbasic-tokens";
|
||||
|
||||
// ── Lookup sets (lower-cased) ───────────────────────────────────────
|
||||
const kwSet = wordSet(KEYWORDS);
|
||||
const opsSet = wordSet(LOGICAL_OPERATORS);
|
||||
const constsSet = wordSet(CONSTANTS);
|
||||
const fnsSet = wordSet(FUNCTIONS);
|
||||
const preprocSet = new Set(PREPROCESSOR.map((p) => p.toLowerCase()));
|
||||
|
||||
// ── Token types → CSS class mapping ────────────────────────────────
|
||||
export type TokenType =
|
||||
| "keyword"
|
||||
| "builtin"
|
||||
| "operator"
|
||||
| "atom"
|
||||
| "number"
|
||||
| "string"
|
||||
| "comment"
|
||||
| "meta";
|
||||
|
||||
export interface Token {
|
||||
from: number;
|
||||
to: number;
|
||||
type: TokenType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize a single line of CRBasic source.
|
||||
* `offset` is the absolute position of the line start (for CM6 decorations).
|
||||
*/
|
||||
export function tokenizeLine(line: string, offset: number): Token[] {
|
||||
const tokens: Token[] = [];
|
||||
let i = 0;
|
||||
const len = line.length;
|
||||
|
||||
while (i < len) {
|
||||
// Skip whitespace
|
||||
if (/\s/.test(line[i])) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Comment: ' to end of line
|
||||
if (line[i] === "'") {
|
||||
tokens.push({ from: offset + i, to: offset + len, type: "comment" });
|
||||
break;
|
||||
}
|
||||
|
||||
// String: "..."
|
||||
if (line[i] === '"') {
|
||||
const start = i;
|
||||
i++;
|
||||
while (i < len && line[i] !== '"') i++;
|
||||
if (i < len) i++; // consume closing quote
|
||||
tokens.push({ from: offset + start, to: offset + i, type: "string" });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preprocessor: #word
|
||||
if (line[i] === "#") {
|
||||
const m = line.slice(i).match(/^#\w+/i);
|
||||
if (m && preprocSet.has(m[0].toLowerCase())) {
|
||||
tokens.push({ from: offset + i, to: offset + i + m[0].length, type: "meta" });
|
||||
i += m[0].length;
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Numbers
|
||||
if (/\d/.test(line[i])) {
|
||||
const m = line.slice(i).match(/^\d+(\.\d+)?/);
|
||||
if (m) {
|
||||
tokens.push({ from: offset + i, to: offset + i + m[0].length, type: "number" });
|
||||
i += m[0].length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-char operators
|
||||
const opMatch = line.slice(i).match(/^(<>|>>|<<|>=|<=|\*=|\+=|-=|\/=|\\=|\^=|&=)/);
|
||||
if (opMatch) {
|
||||
tokens.push({ from: offset + i, to: offset + i + opMatch[0].length, type: "operator" });
|
||||
i += opMatch[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Single-char operators
|
||||
if (/[+\-*/\\^<>=&@!]/.test(line[i])) {
|
||||
tokens.push({ from: offset + i, to: offset + i + 1, type: "operator" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Identifiers / keywords / functions
|
||||
if (/[A-Za-z_]/.test(line[i])) {
|
||||
const m = line.slice(i).match(/^[A-Za-z_]\w*/);
|
||||
if (m) {
|
||||
const word = m[0].toLowerCase();
|
||||
let type: TokenType | null = null;
|
||||
if (kwSet.has(word)) type = "keyword";
|
||||
else if (opsSet.has(word)) type = "operator";
|
||||
else if (constsSet.has(word)) type = "atom";
|
||||
else if (fnsSet.has(word)) type = "builtin";
|
||||
if (type) {
|
||||
tokens.push({ from: offset + i, to: offset + i + m[0].length, type });
|
||||
}
|
||||
i += m[0].length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Anything else — skip
|
||||
i++;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** CSS class for CM6 decorations */
|
||||
export function cmClass(type: TokenType): string {
|
||||
return `cm-${type}`;
|
||||
}
|
||||
1
styles.css
Normal file
1
styles.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* CodeBlock CRBasic — no custom styles needed; uses Obsidian's theme tokens. */
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"sourceMap": false,
|
||||
"baseUrl": ".",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Reference in a new issue