/** * To tackle challenge of making the complex grammar for typst markup, the grammar is described by neither JSON nor YAML, * but a TypeScript generator program. * * TypeScript ensures correct grammar by static and strong types from [./textmate.ts](./textmate.mts). * * The {@link generate} function outputs the grammar to the JSON files. * - [./typst.tmLanguage.json](./typst.tmLanguage.json) is the grammar for typst in markup mode. * - [./typst-code.tmLanguage.json](./typst-code.tmLanguage.json) is the grammar for typst in code mode. */ import * as textmate from "./textmate.ts"; import { blockRaw, blockRawGeneral, blockRawLangs, inlineRaw } from "./fenced.ts"; import * as fs from "fs/promises"; import * as path from "path"; import { FIXED_LENGTH_LOOK_BEHIND, POLYFILL_P_XID, SYNTAX_WITH_BOLD_ITALIC, SYNTAX_WITH_MATH, } from "./feature.ts"; const { lookAhead, oneOf, replaceGroup, metaName } = textmate; /** * {@link _RegexPart} * {@link _SimplePatternPart} * {@link _CommentPatternPart} * {@link _BlockPatternPart} * {@link _MathModePatternPart} * {@link _MarkupModePatternPart} * {@link _CodeModePatternPart} * {@link _TypstGrammarPart} */ type _Parts = never; /** * Defines regexes. */ type _RegexPart = never; /** * A typst identifier in code mode. */ const IDENTIFIER = /(? { const tokens = [ /while/, /and/, /not/, /if/, /or/, /in/, /!=/, /==/, /<=/, />=/, //, /\+/, /-/, /\*/, /\//, /=>/, /=/, /\+=/, /-=/, /\*=/, /\/=/, ]; let lookBehind = ""; if (!FIXED_LENGTH_LOOK_BEHIND) { const tokenSet = tokens.map((t) => t.source).join("|"); lookBehind = `(?/, />=/, /<=/, /==/ is a sub-regex of /[=<>\+\-\*\/]{2}/ // Also note that, /[=<>\+\-\*\/]{2}/ is a sub-regex of /[\s\S][=<>\+\-\*\/]/ /[\s\S]{4}[=<>\+\-\*\/]/, /[\s\S]{3}[=<>\+\-\*\/]\s/, /[\s\S]{2}[=<>\+\-\*\/]\s{2}/, /[\s\S][=<>\+\-\*\/]\s{3}/, /[=<>\+\-\*\/]\s{4}/, ); lookBehind = `(? { if (!FIXED_LENGTH_LOOK_BEHIND) { return /(?<=[\}\]])|(? new RegExp(FLOAT_OR_INT.source + (canDotSuff ? "" : "(? { return { name: "comment.line.double-slash.typst", begin: strict ? /(? { return { name: "meta.expr.call.typst", begin: lookAhead( new RegExp(`(?:${oneOf(MATH_DOT_ACCESS, MATH_IDENTIFIER).source})` + /(?=\()/.source), ), end: replaceGroup( /(?:(?<=[\)])(?![\(\.]|[CallStart]))|(?=[\$\s;,\}\]\)]|$)/u, "[CallStart]", mathCallStart, ), patterns: [ { match: /\./, name: "keyword.operator.accessor.typst", }, { match: mathCallStart, name: "entity.name.function.typst", captures: { "0": { name: "entity.name.function.typst", patterns: [ { include: "#primitiveFunctions" }, { include: "#primitiveTypes" }, { match: /.*/, name: "entity.name.function.typst", }, ], }, }, }, { include: "#mathIdentifier" }, // empty args { // name: "meta.call.args.typst", match: /(\()\s*(\))/, captures: { "1": { name: "meta.brace.round.typst" }, "2": { name: "meta.brace.round.typst" }, }, }, { include: "#mathCallArgs" }, ], }; }; /** * Defines patterns in common (code and markup) mode. * {@link common} */ type _CommonPatternPart = never; const common: textmate.Pattern = { patterns: [{ include: "#strictComments" }, { include: "#blockRaw" }, { include: "#inlineRaw" }], }; /** * Defines patterns in markup mode. * {@link markup} * {@link boldItalicMarkup} * {@link markupLabel} * {@link markupReference} * {@link markupEscape} * {@link markupHeading} * {@link markupEnterCode} * {@link markupLink} * {@link markupLinkParen} * {@link markupLinkBracket} * {@link markupBold} * {@link markupItalic} * * {@link inlineRaw} * {@link blockRaw} * {@link blockRawGeneral} */ type _MarkupModePatternPart = never; // These two markup are buggy const boldItalicMarkup = SYNTAX_WITH_BOLD_ITALIC ? [{ include: "#markupBold" }, { include: "#markupItalic" }] : []; const markup: textmate.Pattern = { patterns: [ { include: "#common" }, { include: "#markupEnterCode" }, { include: "#markupEscape" }, { name: "punctuation.definition.linebreak.typst", match: /\\/ }, { name: "punctuation.definition.nonbreaking-space.typst", match: /\~/ }, { name: "punctuation.definition.shy.typst", match: /-\?/ }, { name: "punctuation.definition.em-dash.typst", match: /---/ }, { name: "punctuation.definition.en-dash.typst", match: /--/ }, { name: "punctuation.definition.ellipsis.typst", match: /\.\.\./ }, // what is it? // { // name: "constant.symbol.typst", // match: /:([a-zA-Z0-9]+:)+/, // }, ...boldItalicMarkup, { include: "#markupLink" }, { include: "#markupMath" }, { include: "#markupHeading" }, { name: "punctuation.definition.list.unnumbered.typst", match: /^\s*-\s+/ }, { name: "punctuation.definition.list.numbered.typst", match: /^\s*([0-9]+\.|\+)\s+/, }, // The term list parsing is buggy // { // match: /^\s*(\/)\s+([^:]*)(:)/, // captures: { // "1": { // name: "punctuation.definition.list.description.typst", // }, // "2": { // patterns: [ // { // include: "#markup", // }, // ], // }, // "3": { // name: "markup.list.term.typst", // }, // }, // }, { include: "#markupLabel" }, { include: "#markupReference" }, { include: "#markupBrace" }, ], }; const markupLabel: textmate.PatternMatch = { name: "string.other.label.typst", match: /<[\p{XID_Start}_][\p{XID_Continue}_\-\.:]*>/u, }; const markupReference: textmate.PatternMatch = { name: "string.other.reference.typst", match: /(@)[\p{XID_Start}_](?:[\p{XID_Continue}_\-]|[\.:](?!:\s|$|([\.:]*[^\p{XID_Continue}_\-\.:])))*/u, captures: { "1": { name: "punctuation.definition.reference.typst" }, }, }; const markupEscape: textmate.PatternMatch = { name: "constant.character.escape.content.typst", match: /\\(?:[^u]|u\{?[0-9a-zA-Z]*\}?)/, }; const markupHeading: textmate.Pattern = { name: "markup.heading.typst", begin: /^\s*(=+)(?:(?=[\r\n]|$)|[^\S\n]+)/, end: /\n|(?=<)/, beginCaptures: { "1": { name: "punctuation.definition.heading.typst" }, }, patterns: [{ include: "#markup" }], }; const enterExpression = (kind: string, seek: RegExp): textmate.Pattern => { return { // name: "markup.expr.enter.typst", begin: new RegExp("#" + seek.source), end: oneOf( /(?<=;)/, // Ends unless we are in a call or method call new RegExp( /(?<=[\}\]\)])(?![;\(\[\$]|(?:\.method-continue))/.source.replace( /method-continue/g, IDENTIFIER.source + /(?=[\(\[])/.source, ), ), // The hash starts a string or an identifier. /(? { const MARKUP_BOUNDARY = `[\\W\\p{Han}\\p{Hangul}\\p{Katakana}\\p{Hiragana}]`; const notationAtBound = `(?:(^${ch}|${ch}$|((?<=${MARKUP_BOUNDARY})${ch})|(${ch}(?=${MARKUP_BOUNDARY}))))`; return { name: `markup.${style}.typst`, begin: notationAtBound, end: new RegExp(notationAtBound + `|\\n|(?=\\])`), beginCaptures: { "0": { name: `punctuation.definition.${style}.typst` }, }, endCaptures: { "0": { name: `punctuation.definition.${style}.typst` }, }, patterns: [{ include: "#markup" }], }; }; const markupBold = markupAnnotate("\\*", "bold"); const markupItalic = markupAnnotate("_", "italic"); /** * Defines patterns in code mode. * {@link code} * {@link expression} * {@link arrayOrDict} * {@link literalContent} * {@link contextStatement} * {@link includeStatement} * {@link importStatement} * {@link letStatement} * {@link ifStatement} * {@link forStatement} * {@link whileStatement} * {@link setStatement} * {@link showStatement} * {@link callArgs} * {@link patternOrArgsBody} * {@link funcCallOrPropAccess} */ type _CodeModePatternPart = never; const code: textmate.Pattern = { patterns: [ { include: "#common" }, { include: "#comments" }, { name: "punctuation.terminator.statement.typst", match: /;/ }, { include: "#expression" }, ], }; const expression: textmate.Pattern = { patterns: [ { include: "#comments" }, { include: "#arrayOrDict" }, { include: "#contentBlock" }, { match: /\b(else)\b(?!-)/, name: "keyword.control.conditional.typst", }, { match: /\b(break|continue)\b(?!-)/, name: "keyword.control.loop.typst", }, { match: /\b(in)\b(?!-)/, name: "keyword.other.range.typst", }, { match: /\b(and|or|not)\b(?!-)/, name: "keyword.other.logical.typst", }, { match: /\b(return)\b(?!-)/, name: "keyword.control.flow.typst", }, { include: "#markupLabel" }, { include: "#blockRaw" }, { include: "#inlineRaw" }, { include: "#codeBlock" }, { include: "#letStatement" }, { include: "#showStatement" }, { include: "#contextStatement" }, { include: "#setStatement" }, { include: "#forStatement" }, { include: "#whileStatement" }, { include: "#ifStatement" }, { include: "#importStatement" }, { include: "#includeStatement" }, { include: "#strictFuncCallOrPropAccess" }, { include: "#primitiveColors" }, { include: "#primitiveFunctions" }, { include: "#primitiveTypes" }, // todo: enable if only if for completely right grammar { include: "#keywordConstants" }, { include: "#identifier" }, { include: "#constants" }, { include: "#codeMath" }, { match: /(as)\b(?!-)/, name: "keyword.control.typst", }, { match: /(in)\b(?!-)/, name: "keyword.operator.range.typst", }, { match: /\.\./, name: "keyword.operator.spread.typst", }, { match: /:/, name: "punctuation.separator.colon.typst", }, { match: /\./, name: "keyword.operator.accessor.typst", }, { match: /,/, name: "punctuation.separator.comma.typst", }, { match: /=>/, name: "storage.type.function.arrow.typst", }, { match: /==|!=|<=|<|>=|>/, name: "keyword.operator.relational.typst", }, { begin: /(\+=|-=|\*=|\/=|=)/, end: /(?=[\n;\}\]\)])/, beginCaptures: { "1": { name: "keyword.operator.assignment.typst" }, }, patterns: [{ include: "#expression" }], }, { match: /\+|\\|\/|\*|-/, name: "keyword.operator.arithmetic.typst", }, ], }; const arrayOrDict: textmate.Pattern = { patterns: [ /// empty array () { match: /(\()\s*(\))/, captures: { "1": { name: "meta.brace.round.typst" }, "2": { name: "meta.brace.round.typst" }, }, }, /// empty dictionary (:) { match: /(\()\s*(:)\s*(\))/, captures: { "1": { name: "meta.brace.round.typst" }, "2": { name: "punctuation.separator.colon.typst" }, "3": { name: "meta.brace.round.typst" }, }, }, /// parentheisized expressions: (...) { begin: /\(/, end: /\)|(?=[;\}\]])/, beginCaptures: { "0": { name: "meta.brace.round.typst" }, }, endCaptures: { "0": { name: "meta.brace.round.typst" }, }, patterns: [{ include: "#literalContent" }], }, ], }; const literalContent: textmate.Pattern = { patterns: [{ include: "#paramOrArgName" }, { include: "#expression" }], }; const contextStatement: textmate.Pattern = { name: "meta.expr.context.typst", begin: /\bcontext\b(?!-)/, end: contextEndReg(), beginCaptures: { "0": { name: "keyword.control.other.typst" }, }, patterns: [{ include: "#expression" }], }; const includeStatement: textmate.Pattern = { name: "meta.expr.include.typst", begin: /(\binclude\b(?!-))\s*/, end: /(?=[\n;\}\]\)])/, beginCaptures: { "1": { name: "keyword.control.import.typst" }, }, patterns: [ { include: "#comments", }, { include: "#expression", }, ], }; // todo: sometimes eat a character const importStatement = (): textmate.Grammar => { const importStatement: textmate.Pattern = { name: "meta.expr.import.typst", begin: /(\bimport\b(?!-))\s*/, end: /(?=[\n;\}\]\)])/, beginCaptures: { "1": { name: "keyword.control.import.typst" }, }, patterns: [ { include: "#comments" }, { include: "#importPathClause" }, { match: /\:/, name: "punctuation.separator.colon.typst", }, { match: /\*/, name: "keyword.operator.wildcard.typst", }, { match: /\,/, name: "punctuation.separator.comma.typst", }, { include: "#importAsClause" }, { include: "#expression" }, ], }; /// import expression until as|: const importPathClause: textmate.Pattern = { begin: /(\bimport\b(?!-))\s*/, // todo import as end: /(?=\:|as)/, beginCaptures: { "1": { name: "keyword.control.import.typst" }, }, patterns: [{ include: "#comments" }, { include: "#expression" }], }; /// as expression const importAsClause: textmate.Pattern = { // todo: as... begin: /(\bas\b)\s*/, end: /(?=[\s;\}\]\)\:])/, beginCaptures: { "1": { name: "keyword.control.import.typst" }, }, patterns: [{ include: "#comments" }, { include: "#identifier" }], }; return { repository: { importStatement, importPathClause, importAsClause, }, }; }; const letStatement = (): textmate.Grammar => { const letStatement: textmate.Pattern = { name: "meta.expr.let.typst", begin: lookAhead(/(let\b(?!-))/), end: /(?!\=)(?=[\s;\}\]\)])/, patterns: [ /// Matches any comments { include: "#comments" }, /// Matches binding clause { include: "#letBindingClause" }, /// Matches init assignment clause { include: "#letInitClause" }, ], }; const letBindingClause: textmate.Pattern = { // name: "meta.let.binding.typst", begin: /(let\b(?!-))\s*/, end: /(?=[=;\]}\n])/, beginCaptures: { "1": { name: "storage.type.typst" }, }, patterns: [ { include: "#comments" }, /// Matches a func call after the let identifier { begin: /(\b[\p{XID_Start}_][\p{XID_Continue}_\-]*)(\()/u, end: /\)|(?=[;\}\]])/, beginCaptures: { "1": { name: "entity.name.function.typst", patterns: [{ include: "#primitiveFunctions" }], }, "2": { name: "meta.brace.round.typst" }, }, endCaptures: { "0": { name: "meta.brace.round.typst" }, }, patterns: [ { include: "#patternOrArgsBody", }, ], }, { begin: /\(/, end: /\)|(?=[;\}\]])/, beginCaptures: { "0": { name: "meta.brace.round.typst" }, }, endCaptures: { "0": { name: "meta.brace.round.typst" }, }, patterns: [{ include: "#patternOrArgsBody" }], }, { include: "#identifier" }, ], }; const letInitClause: textmate.Pattern = { // name: "meta.let.init.typst", begin: /=\s*/, end: /(?=[\n;\}\]\)])/, beginCaptures: { "0": { name: "keyword.operator.assignment.typst" }, }, patterns: [{ include: "#comments" }, { include: "#expression" }], }; return { repository: { letStatement, letBindingClause, letInitClause, }, }; }; /** * Matches a (strict grammar) if in markup context. */ const ifStatement = (): textmate.Grammar => { const ifStatement: textmate.Pattern = { name: metaName("meta.expr.if.typst"), begin: lookAhead(/(else\s+)?(if\b(?!-))/), end: /(?<=\}|\])(?!\s*(else)\b(?!-)|[\[\{])|(?<=else)(?!\s*(?:if\b(?!-)|[\[\{]))|(?=[$;\}\]\)\n]|$)/, patterns: [ { include: "#comments" }, { include: "#ifClause" }, { include: "#elseClause" }, { include: "#codeBlock" }, { include: "#contentBlock" }, ], }; const ifClause: textmate.Pattern = { // name: "meta.if.clause.typst", begin: /\bif\b(?!-)/, end: exprEndIfReg, beginCaptures: { "0": { name: "keyword.control.conditional.typst" }, }, patterns: [{ include: "#expression" }], }; const elseClause: textmate.Pattern = { match: /\belse\b(?!-)/, name: "keyword.control.conditional.typst", }; return { repository: { ifStatement, ifClause, elseClause, }, }; }; const forStatement = (): textmate.Grammar => { // for v in expr { ... } const forStatement: textmate.Pattern = { name: "meta.expr.for.typst", begin: lookAhead(/(for\b(?!-))\s*/), end: /(?<=[\}\]])(?![\{\[])|(?=[$;\}\]\)\n]|$)/, patterns: [ { include: "#comments" }, { include: "#forClause" }, { include: "#codeBlock" }, { include: "#contentBlock" }, ], }; const forClause: textmate.Pattern = { // name: "meta.for.clause.bind.typst", begin: /(for\b)\s*/, end: exprEndForReg, beginCaptures: { "1": { name: "keyword.control.loop.typst" }, }, patterns: [{ include: "#expression" }], }; return { repository: { forStatement, forClause, }, }; }; const whileStatement = (): textmate.Grammar => { // for v in expr { ... } const whileStatement: textmate.Pattern = { name: "meta.expr.while.typst", begin: lookAhead(/(while\b(?!-))/), end: /(?<=[\}\]])(?![\{\[])|(?=[$;\}\]\)\n]|$)/, patterns: [ { include: "#comments" }, { include: "#whileClause" }, { include: "#codeBlock" }, { include: "#contentBlock" }, ], }; const whileClause: textmate.Pattern = { // name: "meta.while.clause.bind.typst", begin: /(while\b)\s*/, end: exprEndWhileReg, beginCaptures: { "1": { name: "keyword.control.loop.typst" }, }, patterns: [{ include: "#expression" }], }; return { repository: { whileStatement, whileClause, }, }; }; const setStatement = (): textmate.Grammar => { const setStatement: textmate.Pattern = { name: "meta.expr.set.typst", begin: lookAhead(new RegExp(/(set\b(?!-))\s*/.source + IDENTIFIER.source)), end: /(?<=\))(?!\s*if\b)|(?=[$\s;\{\[\}\]\)])/, patterns: [ /// Matches any comments { include: "#comments" }, /// Matches binding clause { include: "#setClause" }, /// Matches condition after the set clause { include: "#setIfClause" }, ], }; const setClause: textmate.Pattern = { // name: "meta.set.clause.bind.typst", begin: /(set\b)\s+/, end: /(?=if)|(?=[$\n;\{\[\}\]\)])/, beginCaptures: { "1": { name: "keyword.control.other.typst" }, }, patterns: [ { include: "#comments" }, /// Matches a func call after the set clause { include: "#strictFuncCallOrPropAccess" }, { include: "#identifier" }, ], }; const setIfClause: textmate.Pattern = { // name: "meta.set.if.clause.cond.typst", begin: /(if\b(?!-))\s*/, end: /(?<=\S)(?=|<|>|\+|-|\*|\/|=|\+=|-=|\*=|\/=)(?!\s*(?:and|or|not|in|!=|==|<=|>=|<|>|\+|-|\*|\/|=|\+=|-=|\*=|\/=|\.))|(?=[\n;\}\]\)])/, beginCaptures: { "1": { name: "keyword.control.conditional.typst" }, }, patterns: [{ include: "#comments" }, { include: "#expression" }], }; return { repository: { setStatement, setClause, setIfClause, }, }; }; const showStatement = (): textmate.Grammar => { const showStatement: textmate.Pattern = { name: "meta.expr.show.typst", begin: lookAhead(/(show\b(?!-))/), end: /(?=[\$\s;\{\[\}\]\)])/, patterns: [ /// Matches any comments { include: "#comments" }, /// Matches show any clause { include: "#showAnyClause" }, /// Matches select clause { include: "#showSelectClause" }, /// Matches substitution clause { include: "#showSubstClause" }, ], }; const showAnyClause: textmate.Pattern = { // name: "meta.show.clause.select.typst", match: /(show\b)\s*(?=\:)/, captures: { "1": { name: "keyword.control.other.typst" }, }, }; const showSelectClause: textmate.Pattern = { // name: "meta.show.clause.select.typst", begin: /(show\b)\s*/, end: /(?=[$:;\}\]\n])/, beginCaptures: { "1": { name: "keyword.control.other.typst" }, }, patterns: [ { include: "#comments" }, { include: "#markupLabel" }, /// Matches a func call after the set clause { include: "#expression" }, ], }; const showSubstClause: textmate.Pattern = { // name: "meta.show.clause.subst.typst", begin: /(\:)\s*/, end: /(?=[\n;\}\]\)])/, beginCaptures: { "1": { name: "punctuation.separator.colon.typst" }, }, patterns: [{ include: "#comments" }, { include: "#expression" }], }; return { repository: { showStatement, showAnyClause, showSelectClause, showSubstClause, }, }; }; // todo: { f }(..args) // todo: ( f )(..args) const callArgs: textmate.Pattern = { // name: "meta.call.args.typst", begin: /\(/, end: /\)|(?=[;\}\]])/, beginCaptures: { "0": { name: "meta.brace.round.typst" }, }, endCaptures: { "0": { name: "meta.brace.round.typst" }, }, patterns: [{ include: "#patternOrArgsBody" }], }; const patternOrArgsBody: textmate.Pattern = { patterns: [{ include: "#comments" }, { include: "#paramOrArgName" }, { include: "#expression" }], }; const funcCallOrPropAccess = (strict: boolean): textmate.Pattern => { return { name: "meta.expr.call.typst", begin: lookAhead( strict ? new RegExp(/(\.)?/.source + IDENTIFIER.source + /(?=\(|\[)/.source) : new RegExp(/(\.\s*)?/.source + IDENTIFIER.source + /\s*(?=\(|\[)/.source), ), end: strict ? /(?:(?<=\)|\])(?![\[\(\.]))|(?=[\$\s;,\}\]\)\#]|$)/ : /(?:(?<=\)|\])(?![\[\(\.]))|(?=[\$\n;,\}\]\)\#]|$)/, patterns: [ { match: /\./, name: "keyword.operator.accessor.typst", }, { match: new RegExp( IDENTIFIER.source + (strict ? /(?=\(|\[)/.source : /\s*(?=\(|\[)/.source), ), captures: { "0": { patterns: [ { include: "#primitiveFunctions" }, { include: "#primitiveTypes" }, { match: /.*/, name: "entity.name.function.typst", }, ], }, }, }, { include: "#identifier" }, // empty args { // name: "meta.call.args.typst", match: /(\()\s*(\))/, captures: { "1": { name: "meta.brace.round.typst" }, "2": { name: "meta.brace.round.typst" }, }, }, { include: "#callArgs" }, { include: "#contentBlock" }, ], }; }; /** * Composite and generate the grammar * {@link typst} * {@link generate} */ type _TypstGrammarPart = never; export const typst: textmate.Grammar = { repository: { common, math, markup, shebang, code, comments, codeBlock, codeMath, contentBlock, keywordConstants, constants, primitiveColors, primitiveFunctions, primitiveTypes, identifier, paramOrArgName, stringLiteral, strictComments, blockComment, lineComment, strictLineComment, mathIdentifier, mathBrace, mathParen, mathPrimary, mathMoreBrace, mathCallArgs, strictMathFuncCallOrPropAccess: mathFuncCallOrPropAccess(), markupBrace, markupMath, markupLabel, markupReference, markupEscape, markupHeading, markupEnterCode, markupBold, markupLink, markupLinkParen, markupLinkBracket, markupItalic, inlineRaw, blockRaw, ...blockRawLangs.reduce((acc: Record, lang) => { acc[lang.lang.replace(/\./g, "_")] = lang; return acc; }, {}), blockRawGeneral, expression, arrayOrDict, literalContent, contextStatement, includeStatement, ...importStatement().repository, ...letStatement().repository, ...ifStatement().repository, ...forStatement().repository, ...whileStatement().repository, ...setStatement().repository, ...showStatement().repository, callArgs, patternOrArgsBody, strictFuncCallOrPropAccess: funcCallOrPropAccess(true), // todo: distinguish strict and non-strict for markup and code mode. // funcCallOrPropAccess: funcCallOrPropAccess(false), }, }; export async function generate() { let compiled = textmate.compile(typst); if (POLYFILL_P_XID) { // GitHub PCRE does not support \p{XID_Start} and \p{XID_Continue} // todo: what is Other_ID_Start and Other_ID_Continue? // See, https://unicode.org/Public/UCD/latest/ucd/PropList.txt // \u{309B}\u{309C} const pXIDStart = /\p{L}\p{Nl}_/u; // \u{00B7} \u{30FB} \u{FF65} const pXIDContinue = /\p{L}\p{Nl}\p{Mn}\p{Mc}\p{Nd}\p{Pc}/u; const jsonEncode = (str: string) => { return str.replace(/\\p/g, "\\\\p").replace(/\\u/g, "\\\\u"); }; compiled = compiled .replace(/\\\\p\{XID_Start\}/g, jsonEncode(pXIDStart.source)) .replace(/\\\\p\{XID_Continue\}/g, jsonEncode(pXIDContinue.source)); } const repository = JSON.parse(compiled).repository; await Promise.all([ // dump to file fs.writeFile( path.join(import.meta.dirname, "typst.tmLanguage.json"), JSON.stringify({ $schema: "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", scopeName: "source.typst", name: "typst", patterns: [{ include: "#shebang" }, { include: "#markup" }], repository, }), ), // dump to file fs.writeFile( path.join(import.meta.dirname, "typst-code.tmLanguage.json"), JSON.stringify({ $schema: "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", scopeName: "source.typst-code", name: "typst-code", patterns: [{ include: "#code" }], repository, }), ), ]); } /** * Installs a language to the vscode extension */ async function installLanguage(id: string): Promise { const filePath = path.join(import.meta.dirname, `${id}.tmLanguage.json`); const data = await fs.readFile(filePath, "utf8"); const json = JSON.parse(data); const outPath = path.join(import.meta.dirname, `../../editors/vscode/out/${id}.tmLanguage.json`); await fs.writeFile(outPath, JSON.stringify(json, null, 4), "utf8"); } /** * Installs all languages to the vscode extension */ export async function install() { await fs.mkdir(path.join(import.meta.dirname, `../../editors/vscode/out`), { recursive: true }); await Promise.all([installLanguage("typst"), installLanguage("typst-code")]); } // todo: this is fixed in v0.11.0 // #code(```typ // #let a = 1; #let b = 2; // #(a, b) = (4, 5) // #a, #b // ```)