diff --git a/ghost/.claude-plugin/plugin.json b/ghost/.claude-plugin/plugin.json
index 7affe9d..e574455 100644
--- a/ghost/.claude-plugin/plugin.json
+++ b/ghost/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "ghost",
- "version": "0.1.2",
+ "version": "0.1.3",
"description": "Write, revise, and push blog posts to a Ghost site from Claude Code — a Ghost Admin API MCP plus a plan→draft→revise→push skill set.",
"author": {
"name": "Court Schuett"
diff --git a/ghost/mcp-server/package.json b/ghost/mcp-server/package.json
index 411ad94..dd7cd69 100644
--- a/ghost/mcp-server/package.json
+++ b/ghost/mcp-server/package.json
@@ -1,6 +1,6 @@
{
"name": "ghost-blog-mcp",
- "version": "0.1.2",
+ "version": "0.1.3",
"description": "MCP server for the Ghost Admin API",
"type": "module",
"repository": {
diff --git a/ghost/mcp-server/src/core/lexical-builder.ts b/ghost/mcp-server/src/core/lexical-builder.ts
index 73014be..78b32b5 100644
--- a/ghost/mcp-server/src/core/lexical-builder.ts
+++ b/ghost/mcp-server/src/core/lexical-builder.ts
@@ -11,26 +11,44 @@ interface Part {
const TABLE_RE = /
/g;
const CARD_MARKER = "";
+// Fenced ``` blocks and single-line inline `code`, masked before boundary
+// detection so a or written *inside* code — e.g. a post
+// documenting the card syntax — isn't mistaken for a real card boundary.
+const CODE_SPAN_RE = /```[\s\S]*?```|`[^`\n]*`/g;
+// NUL never occurs in markdown, so mask tokens can't collide with prose.
+const NUL = String.fromCharCode(0);
+const TOKEN_RE = new RegExp(`${NUL}(\\d+)${NUL}`, "g");
// Split markdown into Ghost lexical cards. Top-level blocks become
// their own `html` cards (preserving inline styling and keeping each prose
// chunk between tables independently editable in the Ghost UI); the prose
-// between them is further split on explicit markers. Without
-// either boundary, the whole post is one markdown card.
+// between them is further split on explicit markers. Code spans
+// are masked first so boundaries appearing inside them are left intact.
+// Without either boundary, the whole post is one markdown card.
export function buildLexical(markdown: string): LexicalResult {
+ const codeSpans: string[] = [];
+ const masked = markdown.replace(CODE_SPAN_RE, (span) => {
+ const token = `${NUL}${codeSpans.length}${NUL}`;
+ codeSpans.push(span);
+ return token;
+ });
+ const restore = (s: string): string =>
+ s.replace(TOKEN_RE, (_, i) => codeSpans[Number(i)]);
+
const parts: Part[] = [];
let lastIndex = 0;
let m: RegExpExecArray | null;
TABLE_RE.lastIndex = 0;
- while ((m = TABLE_RE.exec(markdown)) !== null) {
- pushMarkdownParts(parts, markdown.slice(lastIndex, m.index));
+ while ((m = TABLE_RE.exec(masked)) !== null) {
+ pushMarkdownParts(parts, masked.slice(lastIndex, m.index));
parts.push({ type: "html", content: m[0] });
lastIndex = m.index + m[0].length;
}
- pushMarkdownParts(parts, markdown.slice(lastIndex));
+ pushMarkdownParts(parts, masked.slice(lastIndex));
const children = parts
+ .map((p) => ({ type: p.type, content: restore(p.content) }))
.filter((p) => p.content.trim().length > 0)
.map((p) =>
p.type === "markdown"
diff --git a/ghost/mcp-server/test/lexical-builder.test.ts b/ghost/mcp-server/test/lexical-builder.test.ts
index 87df98f..0322f93 100644
--- a/ghost/mcp-server/test/lexical-builder.test.ts
+++ b/ghost/mcp-server/test/lexical-builder.test.ts
@@ -35,4 +35,35 @@ describe("buildLexical", () => {
const root = JSON.parse(buildLexical("Hi.").lexical).root;
expect(root).toMatchObject({ type: "root", version: 1, indent: 0 });
});
+
+ it("ignores inside inline code", () => {
+ const r = buildLexical("An explicit `` marker in prose.");
+ expect(r.cardCount).toBe(1);
+ expect(r.cardSummary).toBe("markdown");
+ });
+
+ it("ignores and inside a fenced block", () => {
+ const md =
+ "Before.\n\n```html\n\n" +
+ "\n```\n\nAfter.";
+ const r = buildLexical(md);
+ expect(r.cardCount).toBe(1);
+ expect(r.cardSummary).toBe("markdown");
+ });
+
+ it("keeps a real but ignores one shown in inline code", () => {
+ const md =
+ "Shown: ``.\n\n" +
+ "\n\nEnd.";
+ const r = buildLexical(md);
+ expect(r.cardSummary).toBe("markdown, html, markdown");
+ expect(r.cardCount).toBe(3);
+ });
+
+ it("restores code verbatim without mangling nearby numbers", () => {
+ const md = "A key is 24 hex chars and `` sits mid-line.";
+ const child = children(buildLexical(md))[0] as { markdown: string };
+ expect(child.markdown).toContain("24 hex chars");
+ expect(child.markdown).toContain("``");
+ });
});