Skip to content

Commit

Permalink
Lua: multi-line string literals
Browse files Browse the repository at this point in the history
  • Loading branch information
zefhemel committed Oct 7, 2024
1 parent 41a8566 commit 6c71862
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 24 deletions.
1 change: 0 additions & 1 deletion common/space_lua/eval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export function evalExpression(
try {
switch (e.type) {
case "String":
// TODO: Deal with escape sequences
return e.value;
case "Number":
return e.value;
Expand Down
14 changes: 9 additions & 5 deletions common/space_lua/lua.grammar
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ exp {
UnaryExpression |
TableConstructor |
FunctionDef { kw<"function"> FuncBody }
/*| Query*/
// | Query
}

Query {
Expand Down Expand Up @@ -154,22 +154,26 @@ TableConstructor { "{" (field (fieldsep field)* fieldsep?)? "}" }
@tokens {
CompareOp { "<" | ">" | $[<>=~/!] "=" }


TagIdentifier { @asciiLetter (@asciiLetter | @digit | "-" | "_" | "/" )* }


word { (std.asciiLetter | "_") (std.digit | std.asciiLetter | "_")* }

identifier { word }

stringEscape {
"\\" ($[abfnz"'\\] | digit digit? digit?) |
"\\x" hex hex |
// NOTE: this should really be /[0-7]hex{5}/ at max, but that's annoying to write
"\\u{" hex+ "}"
}

simpleString { "'" (stringEscape | ![\r\n\\'])* "'" | '"' (stringEscape | ![\r\n\\"])* '"'}
// Any sequence of characters except two consecutive ]]
longStringContent { (![\]] | $[\]] ![\]])* }

simpleString {
"'" (stringEscape | ![\r\n\\'])* "'" |
'"' (stringEscape | ![\r\n\\"])* '"' |
'[[' longStringContent ']]'
}

hex { $[0-9a-fA-F] }
digit { std.digit }
Expand Down
2 changes: 1 addition & 1 deletion common/space_lua/parse-lua.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion common/space_lua/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Deno.test("Test Lua parser", () => {
parse(
`e(1, 1.2, -3.8, +4, #lst, true, false, nil, "string", "", "Hello there \x00", ...)`,
);
parse(`e([[hel]lo]], "Grinny face\\u{1F600}")`);

parse(`e(10 << 10, 10 >> 10, 10 & 10, 10 | 10, 10 ~ 10)`);

Expand Down Expand Up @@ -92,5 +93,8 @@ Deno.test("Test comment handling", () => {
--[[ Multi
line
comment ]]
f()`);
f([[
hello
-- yo
]])`);
});
86 changes: 77 additions & 9 deletions common/space_lua/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,56 @@ function parseExpList(t: ParseTree, ctx: ASTCtx): LuaExpression[] {
);
}

// In case of quoted strings, remove the quotes and unescape the string
// In case of a [[ type ]] literal string, remove the brackets
function parseString(s: string): string {
if (s.startsWith("[[") && s.endsWith("]]")) {
return s.slice(2, -2);
}
return s.slice(1, -1).replace(
/\\(x[0-9a-fA-F]{2}|u\{[0-9a-fA-F]+\}|[abfnrtv\\'"n])/g,
(match, capture) => {
switch (capture) {
case "a":
return "\x07"; // Bell
case "b":
return "\b"; // Backspace
case "f":
return "\f"; // Form feed
case "n":
return "\n"; // Newline
case "r":
return "\r"; // Carriage return
case "t":
return "\t"; // Horizontal tab
case "v":
return "\v"; // Vertical tab
case "\\":
return "\\"; // Backslash
case '"':
return '"'; // Double quote
case "'":
return "'"; // Single quote
default:
// Handle hexadecimal \x00
if (capture.startsWith("x")) {
return String.fromCharCode(parseInt(capture.slice(1), 16));
}
// Handle unicode \u{XXXX}
if (capture.startsWith("u{")) {
const codePoint = parseInt(capture.slice(2, -1), 16);
return String.fromCodePoint(codePoint);
}
return match; // return the original match if nothing fits
}
},
);
}

function parseExpression(t: ParseTree, ctx: ASTCtx): LuaExpression {
switch (t.type) {
case "LiteralString": {
let cleanString = t.children![0].text!;
// Remove quotes etc
cleanString = cleanString.slice(1, -1);
const cleanString = parseString(t.children![0].text!);
return {
type: "String",
value: cleanString,
Expand Down Expand Up @@ -506,22 +550,46 @@ function parseTableField(t: ParseTree, ctx: ASTCtx): LuaTableField {
throw new Error(`Unknown table field type: ${t.type}`);
}
}

function stripLuaComments(s: string): string {
// Strips Lua comments (single-line and multi-line) and replaces them with equivalent length whitespace
let result = "";
let inString = false;
let inMultilineString = false;
let inComment = false;
let inMultilineComment = false;

for (let i = 0; i < s.length; i++) {
// Handle string detection (to avoid stripping comments inside strings)
if (s[i] === '"' && !inComment && !inMultilineComment) {
// Handle string detection for single-line strings (to avoid stripping comments inside strings)
if (
s[i] === '"' && !inComment && !inMultilineComment && !inMultilineString
) {
inString = !inString;
}

// Handle multi-line string literals (starting with "[[")
if (
!inString && !inComment && !inMultilineComment && s[i] === "[" &&
s[i + 1] === "["
) {
inMultilineString = true;
result += "[["; // Copy "[[" into result
i += 1; // Skip over "[["
continue;
}

// Handle end of multi-line string literals (ending with "]]")
if (inMultilineString && s[i] === "]" && s[i + 1] === "]") {
inMultilineString = false;
result += "]]"; // Copy "]]" into result
i += 1; // Skip over "]]"
continue;
}

// Handle single-line comments (starting with "--")
if (!inString && !inMultilineComment && s[i] === "-" && s[i + 1] === "-") {
if (
!inString && !inMultilineString && !inMultilineComment && s[i] === "-" &&
s[i + 1] === "-"
) {
if (s[i + 2] === "[" && s[i + 3] === "[") {
// Detect multi-line comment start "--[["
inMultilineComment = true;
Expand All @@ -546,9 +614,9 @@ function stripLuaComments(s: string): string {
continue;
}

// Replace comment content with spaces, or copy original content if not in comment
// Replace comment content with spaces, or copy original content if not in comment or multi-line string
if (inComment || inMultilineComment) {
result += " "; // Replace comment characters with a space
result += " "; // Replace comment characters with spaces
} else {
result += s[i];
}
Expand Down
4 changes: 3 additions & 1 deletion plug-api/lib/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ export function cleanTree(tree: ParseTree, omitTrimmable = true): ParseTree {
const parseErrorNodes = collectNodesOfType(tree, "⚠");
if (parseErrorNodes.length > 0) {
throw new Error(
`Parse error in: ${renderToText(tree)}`,
`Parse error (${parseErrorNodes[0].from}:${parseErrorNodes[0].to}): ${
renderToText(tree)
}`,
);
}
if (tree.text !== undefined) {
Expand Down
14 changes: 12 additions & 2 deletions plugs/index/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,19 @@ export async function lintLua({ tree }: LintEvent): Promise<LintDiagnostic[]> {
try {
await lua.parse(luaCode);
} catch (e: any) {
const offset = codeText.from!;
let from = codeText.from!;
let to = codeText.to!;
if (e.message.includes("Parse error (")) {
const errorMatch = errorRegex.exec(e.message);
if (errorMatch) {
from = offset + parseInt(errorMatch[1], 10);
to = offset + parseInt(errorMatch[2], 10);
}
}
diagnostics.push({
from: codeText.from!,
to: codeText.to!,
from,
to,
severity: "error",
message: e.message,
});
Expand Down
37 changes: 33 additions & 4 deletions web/cm_plugins/lua_directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,25 @@ import {
shouldRenderWidgets,
} from "./util.ts";
import type { Client } from "../client.ts";
import { parse } from "$common/space_lua/parse.ts";
import { parse as parseLua } from "$common/space_lua/parse.ts";
import type {
LuaBlock,
LuaFunctionCallStatement,
} from "$common/space_lua/ast.ts";
import { evalExpression } from "$common/space_lua/eval.ts";
import { luaToString } from "$common/space_lua/runtime.ts";
import { parse as parseMarkdown } from "$common/markdown_parser/parse_tree.ts";
import { extendedMarkdownLanguage } from "$common/markdown_parser/parser.ts";
import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts";
import {
isLocalPath,
resolvePath,
} from "@silverbulletmd/silverbullet/lib/resolve";

class LuaDirectiveWidget extends WidgetType {
constructor(
readonly code: string,
private client: Client,
) {
super();
}
Expand All @@ -38,13 +46,34 @@ class LuaDirectiveWidget extends WidgetType {
const span = document.createElement("span");
span.className = "sb-lua-directive";
try {
const parsedLua = parse(`_(${this.code})`) as LuaBlock;
const parsedLua = parseLua(`_(${this.code})`) as LuaBlock;
const expr =
(parsedLua.statements[0] as LuaFunctionCallStatement).call.args[0];

Promise.resolve(evalExpression(expr, client.clientSystem.spaceLuaEnv.env))
.then((result) => {
span.innerText = luaToString(result);
const mdTree = parseMarkdown(
extendedMarkdownLanguage,
luaToString(result),
);

const html = renderMarkdownToHtml(mdTree, {
// Annotate every element with its position so we can use it to put
// the cursor there when the user clicks on the table.
annotationPositions: true,
translateUrls: (url) => {
if (isLocalPath(url)) {
url = resolvePath(
this.client.currentPage,
decodeURI(url),
);
}

return url;
},
preserveAttributes: true,
}, this.client.ui.viewState.allPages);
span.innerHTML = html;
}).catch((e) => {
console.error("Lua eval error", e);
span.innerText = `Lua error: ${e.message}`;
Expand Down Expand Up @@ -81,7 +110,7 @@ export function luaDirectivePlugin(client: Client) {

widgets.push(
Decoration.widget({
widget: new LuaDirectiveWidget(text),
widget: new LuaDirectiveWidget(text, client),
}).range(node.to),
);
widgets.push(invisibleDecoration.range(node.from, node.to));
Expand Down

0 comments on commit 6c71862

Please sign in to comment.