fix(tutor): find gloss blocks in the raw text — the new parse() still leaks

The 16 Sep lib fixed both defects gloss.ts used to work around: a gloss
block closes at each "=" line, and every block is collected. So the
sentence splitting goes.

Three defects remain in how a block ENDS, measured on the new lib:

  · deleting one block still deletes the "::" of the next, so a whole
    ::words block reaches the prose as "words" plus its rows;
  · an unclosed gloss still swallows the following paragraph as parts;
  · two gloss blocks written back to back share one terminator, and lib
    never sees the second.

The old rebuild also only re-parsed the FIRST gloss block, which against
a lib that accumulates would have dropped every block after it.

One pass over the raw text now yields both the prose and each gloss
block's content, a gloss block ending where its rows end; lib parses each
block on its own, so the row format is still entirely lib's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-09-16 19:50:11 +02:00
parent e72b77d6c2
commit ba3055912a
3 changed files with 102 additions and 121 deletions

View File

@@ -1,8 +1,7 @@
/* The ::task block leaking into the message body.
/* Blocks leaking into the message body.
Reported from the app: the raw text "::task translate" and its five
sentences appeared as prose above the exercise those same lines had
already been rendered into.
Reported from the app, twice: raw exercise markup appeared as prose above
the exercise it had already been rendered into.
It is lib/blocks.js, not the model. parse() deletes each block with a
regex whose terminator (?:\n::|$) is part of the match, so deleting one
@@ -180,6 +179,11 @@ describe("an unclosed ::gloss block", () => {
"나 바다",
].join("\n");
it("lib alone reads the following prose as gloss parts — pinned as it is", () => {
const parts = (parse(RAW).gloss ?? []).flatMap((g) => g.parts.map((p) => p.ko));
expect(parts).toContain("Now practice these.");
});
it("ends the block where its rows end", () => {
expect(glossContent("나 | S | I\n= I sea\n\nprose here\nmore prose")).toBe("나 | S | I\n= I sea\n");
});
@@ -201,6 +205,14 @@ describe("an unclosed ::gloss block", () => {
expect(body).not.toMatch(/::/);
});
it("finds a second gloss block written straight after the first", () => {
// Both blocks share one terminator, so lib alone never sees the second.
const src = "::gloss\n나 | S | I\n= I.\n::gloss\n밥 | O | rice\n= Rice.\n::";
expect(parse(src).gloss).toHaveLength(1);
expect(parseMessage(src).gloss!.map((g) => g.en)).toEqual(["I.", "Rice."]);
expect(parseMessage(src).body).toBe("");
});
it("leaves a properly closed block exactly as lib parses it", () => {
const closed = "::gloss\n물 | N | water\n= Water.\n::\nAfter.";
const g = parseMessage(closed).gloss ?? [];

View File

@@ -1,4 +1,5 @@
/* The multi-sentence gloss workaround. */
/* Gloss blocks as the app renders them: lib parses the rows, parseMessage
decides where each block ends. */
import { describe, it, expect } from "vitest";
import { parseMessage } from "@app/domain/gloss.js";