Implement "quoted words"

This commit is contained in:
Tangent Wantwight 2023-08-23 01:09:56 -04:00
parent 8c3953cdf0
commit ab4cd761b8
4 changed files with 107 additions and 79 deletions

View file

@ -9,10 +9,7 @@ exports[`Parsing Notcl Misc Big mess of markup 1`] = `
"enchanted": "h1",
},
{
"text": ""Hello,",
},
{
"text": "World!"",
"text": "Hello, World!",
},
],
[
@ -47,7 +44,7 @@ exports[`Parsing Notcl Misc Big mess of markup 1`] = `
"enchanted": "-red",
},
{
"text": ""Beware!"",
"text": "Beware!",
},
],
[
@ -55,61 +52,7 @@ exports[`Parsing Notcl Misc Big mess of markup 1`] = `
"enchanted": "para",
},
{
"text": ""All",
},
{
"enchanted": "text",
},
{
"enchanted": "should",
},
{
"enchanted": "be",
},
{
"enchanted": "quoted,",
},
{
"enchanted": "it's",
},
{
"enchanted": "clearer",
},
{
"enchanted": "that",
},
{
"enchanted": "way.",
},
{
"enchanted": "&",
},
{
"enchanted": "blockquotes",
},
{
"enchanted": "already",
},
{
"enchanted": "should",
},
{
"enchanted": "contain",
},
{
"enchanted": "paragraphs.",
},
{
"enchanted": "(maybe",
},
{
"enchanted": "normalize",
},
{
"enchanted": "nested",
},
{
"text": "paragraphs)"",
"text": "All text should be quoted, it's clearer that way. & blockquotes already should contain paragraphs. (maybe normalize nested paragraphs)",
},
],
[

View file

@ -33,12 +33,12 @@ b`)
expect(
parse(String.raw`a\\
b`)
).toEqual([true, [[{ text: "a\\\\" }], [{ enchanted: "b" }]]]));
).toEqual([true, [[{ text: "a\\" }], [{ enchanted: "b" }]]]));
it("does not split commands on folded newlines with escaped backslashes", () =>
expect(
parse(String.raw`a\\\
b`)
).toEqual([true, [[{ text: "a\\\\" }, { enchanted: "b" }]]]));
).toEqual([true, [[{ text: "a\\" }, { enchanted: "b" }]]]));
it("accepts semicolons as command separators", () =>
expect(parse("a;b")).toEqual([
@ -100,7 +100,37 @@ b`)
// no before folded newline that gives us backslashes
});
describe("interpolated words", () => {});
describe("interpolated words", () => {
it("accepts empty quotes", () =>
expect(parse('""')).toEqual([true, [[{ text: "" }]]]));
it("accepts quoted words", () =>
expect(parse('"a"')).toEqual([true, [[{ text: "a" }]]]));
it("accepts quoted words with spaces", () =>
expect(parse('"a b"')).toEqual([true, [[{ text: "a b" }]]]));
it("allows escaped quotes inside a quote", () =>
expect(parse('"a\\"b"')).toEqual([true, [[{ text: 'a"b' }]]]));
it("does not allow trailing characters after a closing quote", () =>
expect(parse('""a')).toMatchObject([false, {}]));
it("accepts escaped spaces", () =>
expect(parse("a\\ b")).toEqual([true, [[{ text: "a b" }]]]));
it("treats a non-leading quote as a plain character", () =>
expect(parse('a"')).toEqual([true, [[{ text: 'a"' }]]]));
it("treats a non-leading brace as a plain character", () =>
expect(parse("a{")).toEqual([true, [[{ text: "a{" }]]]));
it("treats an escaped quote as a plain character", () =>
expect(parse('\\"')).toEqual([true, [[{ text: '"' }]]]));
it("treats an escaped brace as a plain character", () =>
expect(parse("\\{")).toEqual([true, [[{ text: "{" }]]]));
it("treats a quoted brace as a plain character", () =>
expect(parse('"{"')).toEqual([true, [[{ text: "{" }]]]));
// Interpolated words
// variables- bare, brace
// command subst
});
describe("brace words", () => {
it("can parse empty braces", () =>
@ -144,15 +174,6 @@ b`)
).toEqual([true, [[{ text: "\\\\ " }]]]));
});
// "Quotes"
// no trailing chars
// Interpolated words
// bare
// quotes
// variables- bare, brace
// command subst
describe("Misc", () => {
test("Big mess of markup", () => {
expect(

View file

@ -4,6 +4,7 @@ import {
Word as WordType,
TextWord,
EnchantedWord as EnchantedWordType,
SimplifyWord,
} from "./words";
export type Command = WordType[];
@ -19,9 +20,35 @@ const EnchantedWord = Regex(/[^\]\[\}\{$\\";\s]+(?=[\s;]|$)/y)
.map(([enchanted]) => ({ enchanted } as EnchantedWordType))
.expects("ENCHANTED_WORD");
const BasicWord = Regex(/(?!\{)[^\s;]+/y)
.map(([text]) => ({ text } as TextWord))
.expects("BASIC_WORD");
const BackslashEscape = Regex(/\\(.)/y)
.expects("\\")
.map(([, char]) => ({ text: char }));
const BareWord = Sequence(
AtLeast(
1,
Choose(
BackslashEscape,
Regex(/[^\s\\;]+/y)
.expects("CHAR")
.map(([text]) => ({ text }))
)
)
).map(([pieces]) => SimplifyWord(pieces));
const QuotedWord = Sequence(
Regex(/"/y).expects('"'),
AtLeast(
0,
Choose(
BackslashEscape,
Regex(/[^"\\]+/y)
.expects("CHAR")
.map(([text]) => ({ text }))
)
),
Regex(/"/y).expects('"')
).map(([, pieces]) => SimplifyWord(pieces));
const Brace: Pattern<string> = Sequence(
Regex(/\{/y).expects("{"),
@ -40,12 +67,13 @@ const Brace: Pattern<string> = Sequence(
)
),
Regex(/\}/y).expects("}")
).map(([_left, fragments, _right]) => fragments.join(""));
).map(([, fragments]) => fragments.join(""));
const Word = Choose<WordType>(
EnchantedWord,
BasicWord,
Brace.map((text) => ({ text } as TextWord))
Brace.map((text) => ({ text } as TextWord)),
QuotedWord,
BareWord
);
const CommandTerminator = Regex(/[\n;]/y)

View file

@ -29,12 +29,48 @@ export type HtmlWord = {
html: string;
};
export type TextPiece = TextWord;
export type VariablePiece = { variable: string };
export type CommandPiece = { command: unknown };
export type InterpolatedPiece = TextPiece | VariablePiece | CommandPiece;
/**
* A word whose value needs to be determined by evaluating some combination of variable and command
* substitutions, and concatenating the results with any literal spans.
*/
export type InterpolatedWord = {
pieces: [];
pieces: InterpolatedPiece[];
};
function IsTextPiece(piece: InterpolatedPiece | undefined): piece is TextPiece {
return piece ? "text" in piece : false;
}
// safely concatenate text pieces, converting as needed
export function Concat(left: TextPiece, right: TextPiece) {
return { text: left.text + right.text };
}
export function SimplifyWord(
pieces: InterpolatedPiece[]
): InterpolatedWord | TextWord {
const consolidated: InterpolatedPiece[] = [];
for (const piece of pieces) {
const top = consolidated[consolidated.length - 1];
if (IsTextPiece(top) && IsTextPiece(piece)) {
consolidated[consolidated.length - 1] = Concat(top, piece);
} else {
consolidated.push(piece);
}
}
if (consolidated.length == 0) {
return { text: "" };
} else if (consolidated.length == 1 && IsTextPiece(consolidated[0])) {
return consolidated[0];
} else {
return { pieces: consolidated };
}
}
export type Word = EnchantedWord | TextWord | HtmlWord | InterpolatedWord;