-
Notifications
You must be signed in to change notification settings - Fork 545
fix(docs): trim trailing code block blanks #561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package common | ||
|
|
||
| import "strings" | ||
|
|
||
| type markdownFence struct { | ||
| char byte | ||
| size int | ||
| } | ||
|
|
||
| func parseMarkdownFence(line string) (markdownFence, bool) { | ||
| trimmed := strings.TrimSpace(line) | ||
| if len(trimmed) < 3 { | ||
| return markdownFence{}, false | ||
| } | ||
| char := trimmed[0] | ||
| if char != '`' && char != '~' { | ||
| return markdownFence{}, false | ||
| } | ||
| size := 0 | ||
| for size < len(trimmed) && trimmed[size] == char { | ||
| size++ | ||
| } | ||
| if size < 3 { | ||
| return markdownFence{}, false | ||
| } | ||
| return markdownFence{char: char, size: size}, true | ||
| } | ||
|
|
||
| func isMarkdownFenceClose(line string, fence markdownFence) bool { | ||
| trimmed := strings.TrimSpace(line) | ||
| if len(trimmed) < fence.size { | ||
| return false | ||
| } | ||
| size := 0 | ||
| for size < len(trimmed) && trimmed[size] == fence.char { | ||
| size++ | ||
| } | ||
| return size >= fence.size && strings.TrimSpace(trimmed[size:]) == "" | ||
| } | ||
|
|
||
| func isMarkdownBlankLine(line string) bool { | ||
| return strings.TrimSpace(strings.TrimRight(line, "\r\n")) == "" | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
func isMarkdownBlankLine(line string) bool {
return strings.TrimSpace(line) == ""
} |
||
|
|
||
| // TrimMarkdownCodeBlockTrailingBlanks removes blank lines immediately before | ||
| // fenced code block closing markers. Lark's document Markdown round-trip can | ||
| // append one such blank line per fetch/update cycle, causing code blocks to grow. | ||
| func TrimMarkdownCodeBlockTrailingBlanks(markdown string) string { | ||
| if markdown == "" { | ||
| return markdown | ||
| } | ||
|
|
||
| lines := strings.SplitAfter(markdown, "\n") | ||
| out := make([]string, 0, len(lines)) | ||
| pendingBlanks := make([]string, 0, 2) | ||
| var fence markdownFence | ||
| inCodeBlock := false | ||
|
|
||
| for _, line := range lines { | ||
| if !inCodeBlock { | ||
| out = append(out, line) | ||
| if parsed, ok := parseMarkdownFence(line); ok { | ||
| fence = parsed | ||
| inCodeBlock = true | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| if isMarkdownFenceClose(line, fence) { | ||
| pendingBlanks = pendingBlanks[:0] | ||
| out = append(out, line) | ||
| inCodeBlock = false | ||
| continue | ||
| } | ||
|
|
||
| if isMarkdownBlankLine(line) { | ||
| pendingBlanks = append(pendingBlanks, line) | ||
| continue | ||
| } | ||
|
|
||
| if len(pendingBlanks) > 0 { | ||
| out = append(out, pendingBlanks...) | ||
| pendingBlanks = pendingBlanks[:0] | ||
| } | ||
| out = append(out, line) | ||
| } | ||
|
|
||
| if len(pendingBlanks) > 0 { | ||
| out = append(out, pendingBlanks...) | ||
| } | ||
| return strings.Join(out, "") | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two parallel fence parsers now live in the repo — worth a DRY note for a follow-up.
Not a blocker for this PR, but once this lands it'd be worth switching |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package common | ||
|
|
||
| import "testing" | ||
|
|
||
| func TestTrimMarkdownCodeBlockTrailingBlanks(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| in string | ||
| want string | ||
| }{ | ||
| { | ||
| name: "trims one trailing blank line before closing fence", | ||
| in: "before\n```bash\necho hello\n\n```\nafter\n", | ||
| want: "before\n```bash\necho hello\n```\nafter\n", | ||
| }, | ||
| { | ||
| name: "trims accumulated trailing blank lines", | ||
| in: "```go\nfmt.Println(1)\n\n\n```\n", | ||
| want: "```go\nfmt.Println(1)\n```\n", | ||
| }, | ||
| { | ||
| name: "keeps intentional blank lines inside code content", | ||
| in: "```\nline one\n\nline two\n\n```\n", | ||
| want: "```\nline one\n\nline two\n```\n", | ||
| }, | ||
| { | ||
| name: "leaves non-code blank lines alone", | ||
| in: "one\n\n```text\nvalue\n```\n\ntwo\n", | ||
| want: "one\n\n```text\nvalue\n```\n\ntwo\n", | ||
| }, | ||
| { | ||
| name: "supports tilde fences", | ||
| in: "~~~json\n{}\n\n~~~\n", | ||
| want: "~~~json\n{}\n~~~\n", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| tt := tt | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
| if got := TrimMarkdownCodeBlockTrailingBlanks(tt.in); got != tt.want { | ||
| t.Fatalf("TrimMarkdownCodeBlockTrailingBlanks() = %q, want %q", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few test-coverage gaps worth plugging while the test file is fresh:
None of these are likely to be broken in the current implementation, but the test file doubles as a spec for future changes. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,7 +68,7 @@ var DocsCreate = common.Shortcut{ | |
|
|
||
| func buildDocsCreateArgs(runtime *common.RuntimeContext) map[string]interface{} { | ||
| args := map[string]interface{}{ | ||
| "markdown": runtime.Str("markdown"), | ||
| "markdown": common.TrimMarkdownCodeBlockTrailingBlanks(runtime.Str("markdown")), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this actually close issue #53? Issue #53 reports that a freshly-created Feishu document, opened in the Feishu UI, renders an extra blank line at the end of every code block — the user explicitly confirms the markdown sent to the API was already clean. That suggests the blank line is introduced by the MCP This PR normalises markdown on the CLI side of the boundary (trim on create input, trim on fetch/export output). That will:
If the intent is to close #53 as reported, the trim-on-input is a no-op here (the user's source markdown already didn't have the blank). Could we either (a) verify the fix in Feishu UI after this PR and update the issue, or (b) reword the PR / issue link from "Fixes #53" to "Mitigates round-trip growth related to #53" so it's clear the rendering issue remains open? |
||
| } | ||
| if v := runtime.Str("title"); v != "" { | ||
| args["title"] = v | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TrimSpaceover-tolerates indentation — 4-space-indented "fences" are actually indented code block content.CommonMark allows 0–3 spaces of indentation before a fence marker; 4+ spaces means the line is an indented code block inside the surrounding context and the backticks are literal. Because
parseMarkdownFencedoesstrings.TrimSpace(line), a line like" ```"(four leading spaces) is treated as an opening fence, and subsequent lines are then ignored byTrimMarkdownCodeBlockTrailingBlanksuntil it sees a matching close.In practice this is mostly academic for markdown coming out of Lark (server output isn't deeply indented), but it can trigger false positives if a user's source passed through
docs +create/docs +updatenests a literal fence example inside an indented block. Suggest matching CommonMark: count leading spaces, bail out if ≥ 4.Same applies to
isMarkdownFenceCloseat L33.