mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-19 20:44:16 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The web UI uses one shared markdown editor for comments, issue descriptions, and documents. > - Users type `>` at the start of a line to insert a blockquote. > - The live editor shortcut does not always run in every browser and input method. > - The markdown exporter then changes the leading `>` to `\>` and saves literal text. > - The saved text does not render as a blockquote. > - This pull request restores the blockquote marker when markdown enters or leaves the editor. > - The benefit is reliable blockquote insertion on every surface that uses the shared editor. ## Linked Issues or Issue Description No public GitHub issue exists. Related prior attempt: #10465. **What happened?** The shared markdown editor sometimes saved a blockquote as literal text. This happened when the live shortcut did not run. The exporter saved `\> text`, which rendered as literal `> text`. **Expected behavior** A line that starts with `>` must render as a blockquote in comments, issue descriptions, and documents. **Steps to reproduce** 1. Open a task comment composer, description editor, or document editor. 2. Add `> ` to an existing line, or use an input method that does not run the live shortcut. 3. Save the content. 4. Observe that the saved line renders as literal text instead of a blockquote. **Paperclip version or commit** `master` at `78f8c6c3d4`. **Deployment mode** Self-hosted server. ## What Changed - Add `unescapeBlockquoteMarkers()` to restore block-level `\>` markers. - Keep indented code, list content, nested content, and fenced code unchanged. - Apply the helper when markdown enters and leaves `MarkdownEditor`. - Add focused tests for line position, indentation, container prefixes, and CommonMark fence rules. ## Verification - `pnpm exec vitest run ui/src/lib/blockquote-markdown.test.ts` passes with 22 tests. - `pnpm exec vitest run ui/src/components/MarkdownEditor.test.tsx` passes with 37 tests. - `pnpm --filter @paperclipai/ui typecheck` passes. - `pnpm check:token-gates` passes. - `git diff --check origin/master...HEAD` passes. - A browser harness used the real `MarkdownEditor` and `IssueChatThread` composer. It confirmed that `> text` renders as a blockquote and exports as `> text`. - The [Cutter preview](https://github.com/paperclipai/paperclip/pull/10466#issuecomment-5140558263) supplies a task-page screenshot and an editor interaction video. ## Risks - Low risk. The helper returns the input unchanged when it contains no `\>`. - A paragraph that deliberately starts with literal `\>` now becomes a blockquote. The editor has no literal-marker control, so this matches the available input behavior. - There are no database, API, or migration changes. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 4.8 (`claude-opus-4-8`, 1M context), extended thinking, with tool use and code execution. - OpenAI Codex with GPT-5 (`gpt-5`; runtime build and context-window metadata were not exposed), with reasoning, tool use, code execution, and GitHub review tools. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes (none needed) - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
115 lines
5.2 KiB
TypeScript
115 lines
5.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { unescapeBlockquoteMarkers } from "./blockquote-markdown";
|
|
|
|
describe("unescapeBlockquoteMarkers", () => {
|
|
it("leaves markdown without escaped markers untouched", () => {
|
|
expect(unescapeBlockquoteMarkers("> quoted")).toBe("> quoted");
|
|
expect(unescapeBlockquoteMarkers("plain text")).toBe("plain text");
|
|
expect(unescapeBlockquoteMarkers("")).toBe("");
|
|
});
|
|
|
|
it("unescapes a single escaped blockquote line", () => {
|
|
expect(unescapeBlockquoteMarkers("\\> see")).toBe("> see");
|
|
});
|
|
|
|
it("unescapes multiple escaped blockquote lines", () => {
|
|
expect(unescapeBlockquoteMarkers("\\> line one\n\\> line two")).toBe("> line one\n> line two");
|
|
});
|
|
|
|
it("unescapes escaped blockquotes interleaved with normal text", () => {
|
|
expect(unescapeBlockquoteMarkers("hello\n\n\\> quoted\n\nbye")).toBe("hello\n\n> quoted\n\nbye");
|
|
});
|
|
|
|
it("preserves up to 3 spaces of block indent before the marker", () => {
|
|
expect(unescapeBlockquoteMarkers(" \\> indented")).toBe(" > indented");
|
|
expect(unescapeBlockquoteMarkers(" \\> three")).toBe(" > three");
|
|
});
|
|
|
|
it("does not touch an escaped marker in an indented code block (4+ spaces)", () => {
|
|
expect(unescapeBlockquoteMarkers(" \\> literal")).toBe(" \\> literal");
|
|
expect(unescapeBlockquoteMarkers("\tcode \\> x")).toBe("\tcode \\> x");
|
|
});
|
|
|
|
it("does not touch an escaped marker carrying a blockquote container prefix", () => {
|
|
// A code fence nested inside a blockquote has a `> ` prefix on each line, so
|
|
// its `\>` content must stay escaped.
|
|
expect(unescapeBlockquoteMarkers("> \\> nested")).toBe("> \\> nested");
|
|
});
|
|
|
|
it("does not touch an escaped marker inside a list item", () => {
|
|
expect(unescapeBlockquoteMarkers("- \\> item")).toBe("- \\> item");
|
|
});
|
|
|
|
it("does not touch escaped markers inside fenced code blocks", () => {
|
|
const input = "```\n\\> not a quote\n```\n\\> real quote";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("```\n\\> not a quote\n```\n> real quote");
|
|
});
|
|
|
|
it("does not touch escaped markers inside a list-nested fenced code block", () => {
|
|
const input = "- ```\n \\> literal\n ```\n\\> real quote";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("- ```\n \\> literal\n ```\n> real quote");
|
|
});
|
|
|
|
it("does not close a list-nested fence on a list-marker content line", () => {
|
|
const input = "- ```\n - ```\n \\> literal\n ```\n\\> real quote";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("- ```\n - ```\n \\> literal\n ```\n> real quote");
|
|
});
|
|
|
|
it("tracks the continuation indent of an ordered-list fence", () => {
|
|
const input = "10. ```\n \\> literal\n ```\n\\> real quote";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("10. ```\n \\> literal\n ```\n> real quote");
|
|
});
|
|
|
|
it("tracks fenced code blocks through blockquote container prefixes", () => {
|
|
const input = "> ```\n> \\> literal\n> ```\n\\> real quote";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("> ```\n> \\> literal\n> ```\n> real quote");
|
|
});
|
|
|
|
it("handles tilde fences", () => {
|
|
const input = "~~~\n\\> literal\n~~~";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("~~~\n\\> literal\n~~~");
|
|
});
|
|
|
|
it("only rewrites the leading marker, not later text", () => {
|
|
expect(unescapeBlockquoteMarkers("\\> a \\> b")).toBe("> a \\> b");
|
|
});
|
|
|
|
it("leaves a mid-line escaped marker alone", () => {
|
|
expect(unescapeBlockquoteMarkers("text \\> not a quote")).toBe("text \\> not a quote");
|
|
});
|
|
|
|
it("does not close a fence on a shorter same-char run (nested fence content stays code)", () => {
|
|
// A 3-backtick line inside a 4-backtick fence is code content, not a close,
|
|
// so the escaped marker after it must remain escaped.
|
|
const input = "````\n\\> a\n```\n\\> b\n````\n\\> real";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("````\n\\> a\n```\n\\> b\n````\n> real");
|
|
});
|
|
|
|
it("does not treat a fence-like line with trailing content as a closing fence", () => {
|
|
const input = "```\n\\> code\n``` not a close\n\\> still code\n```\n\\> real";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("```\n\\> code\n``` not a close\n\\> still code\n```\n> real");
|
|
});
|
|
|
|
it("does not close a backtick fence with a tilde fence", () => {
|
|
const input = "```\n\\> code\n~~~\n\\> still code\n```\n\\> real";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("```\n\\> code\n~~~\n\\> still code\n```\n> real");
|
|
});
|
|
|
|
it("closes on a longer run than the opening fence", () => {
|
|
const input = "```\n\\> code\n`````\n\\> real";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("```\n\\> code\n`````\n> real");
|
|
});
|
|
|
|
it("treats a backtick fence with a backtick in the info string as non-opening", () => {
|
|
// `` ```` `` with `x` after is not a valid opening fence, so following
|
|
// escaped markers are ordinary paragraph text and get recovered.
|
|
const input = "``` `x`\n\\> real";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("``` `x`\n> real");
|
|
});
|
|
|
|
it("keeps an info string on the opening fence and still protects contents", () => {
|
|
const input = "```ts\n\\> code\n```\n\\> real";
|
|
expect(unescapeBlockquoteMarkers(input)).toBe("```ts\n\\> code\n```\n> real");
|
|
});
|
|
});
|