diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-02 23:43:08 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-03 09:54:39 -0300 |
| commit | 5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e (patch) | |
| tree | f12f4b700fff4ff6f7e818d2a542672ba0b8d76e /vrsite/markdown_test.go | |
| parent | a9263daee9413c5eff3c1f1bebcd224442fd8685 (diff) | |
| download | notevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.tar.gz notevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.zip | |
rebrand to notevi: one CLI over the jj sidecar
vr and vrsite become a single binary. vrsite/ folds into a web package in
one module (0x4200.cafe/notevi); "notevi web" serves and exports exactly
what vrsite did, and "notevi read/grep/note/query" is unchanged.
The sidecar is renamed with it: notevi_log, notevi-log.jsonl, and the
description "private: notevi log". The pre-rebrand names are still
recognized, so an old repository opens and reads; it is renamed in place
on the first write, or up front with "notevi migrate DIR...".
That rename cannot be a single mv inside jj run. jj only auto-tracks a
*new* file in the run working copy below a size limit it does not take
from the command line, so writing a whole log under a name the change has
never held is silently dropped while jj reports success. ensureLogFile
creates the file empty first and lets every later byte be a modification
of a tracked file, which snapshots at any size; that also fixes the same
latent bug when importing a large legacy vr-log.jsonl.
Adds a bem-te-vi mark (favicon and nav brand) and a README.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'vrsite/markdown_test.go')
| -rw-r--r-- | vrsite/markdown_test.go | 245 |
1 files changed, 0 insertions, 245 deletions
diff --git a/vrsite/markdown_test.go b/vrsite/markdown_test.go deleted file mode 100644 index 72f2cca..0000000 --- a/vrsite/markdown_test.go +++ /dev/null @@ -1,245 +0,0 @@ -package main - -import ( - "net/http/httptest" - "net/url" - "strings" - "testing" -) - -func testMarkdown(t *testing.T) *markdown { - t.Helper() - return newMarkdown(newHighlighter(loadTheme("", "", "dark"), loadTheme("", "", "light"))) -} - -func TestMarkdownNotes(t *testing.T) { - m := testMarkdown(t) - cases := []struct { - name string - in string - want []string - not []string - }{ - { - name: "inline marks", - in: "`detect()` prefers **direct env** and only walks the tree as *fallback*", - want: []string{"<code>detect()</code>", "<strong>direct env</strong>", "<em>fallback</em>"}, - }, - { - name: "list after a paragraph", - in: "three classes:\n\n- persisted RPC narrowed\n- ingress bounds change", - want: []string{"<p>three classes:</p>", "<ul>", "<li>persisted RPC narrowed</li>"}, - }, - { - name: "bare url", - in: "see https://example.com/x for the rest", - want: []string{`<a href="https://example.com/x">`}, - }, - { - name: "single newlines are kept", - in: "first line\nsecond line", - want: []string{"first line<br>", "second line"}, - not: []string{"<ul>"}, - }, - // the identifiers real notes are full of must survive untouched: - // underscores are not emphasis, "4.2" is not a list, "@" is not a - // mail address - { - name: "code prose is left alone", - in: "4.2 drops stake_minimum_delegation_for_rewards and remove_simple_vote, unlike @", - want: []string{"stake_minimum_delegation_for_rewards", "remove_simple_vote", "4.2 drops"}, - not: []string{"<em>", "<ol>", "<a href"}, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - got := string(m.render(c.in)) - for _, w := range c.want { - if !strings.Contains(got, w) { - t.Errorf("missing %q in:\n%s", w, got) - } - } - for _, n := range c.not { - if strings.Contains(got, n) { - t.Errorf("unwanted %q in:\n%s", n, got) - } - } - }) - } -} - -func TestNoteLinksFindOnlyLocalMarkdownLinks(t *testing.T) { - p := newNoteLinkParser() - got := noteLinks(p, strings.Join([]string{ - "[root](/note/2)", - "[same target](note/2#discussion)", - "[static relative](../note/3.html?scope=all)", - "`[code](note/4)` and plain note/5 are not links", - "[remote](https://example.com/note/6)", - "[protocol relative](//example.com/note/7)", - "[reference][n]", - "[n]: ./note/8", - }, "\n\n")) - if len(got) != 3 || got[0] != 2 || got[1] != 3 || got[2] != 8 { - t.Errorf("note links = %v, want [2 3 8]", got) - } - - m := testMarkdown(t) - text := "[note](../note/3.html) and [remote](https://example.com/note/4)" - if body := string(m.renderAt(text, &page{Live: true, Root: "/"})); !strings.Contains(body, `href="/note/3"`) || !strings.Contains(body, `href="https://example.com/note/4"`) { - t.Errorf("live note links were not canonicalized selectively:\n%s", body) - } - if body := string(m.renderAt(text, &page{Root: "../"})); !strings.Contains(body, `href="../note/3.html"`) { - t.Errorf("static note link does not follow the page root:\n%s", body) - } -} - -// Angle brackets in a note are code, never markup: they must come through as -// text, and must not stop the markdown around them from being parsed. That -// second half is what goldmark's HTML *block* parser would break — a note -// opening with a quoted tag would render the rest of its paragraph as source. -func TestMarkdownShowsAngleBracketsAsText(t *testing.T) { - m := testMarkdown(t) - cases := []struct{ name, in string }{ - {"inline", "sysvar_cache: RwLock<SysvarCache> — hoisted out of the loop"}, - {"block, tag alone on the line", "<div>\nthe **wrapper** in [page.go](https://go.dev)"}, - {"script", "<script>alert(1)</script> is quoted from [the file](https://go.dev)"}, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - got := string(m.render(c.in)) - if strings.Contains(got, "<div>") || strings.Contains(got, "<script>") { - t.Errorf("markup reached the page:\n%s", got) - } - if strings.Contains(got, "raw HTML omitted") { - t.Errorf("text was dropped:\n%s", got) - } - for _, w := range []string{"<", ">"} { - if !strings.Contains(got, w) { - t.Errorf("missing %q in:\n%s", w, got) - } - } - if strings.Contains(c.in, "[") && !strings.Contains(got, `<a href="https://go.dev">`) { - t.Errorf("markdown stopped being parsed:\n%s", got) - } - }) - } -} - -// A note is written by whoever ran vr, so the renderer is the last thing -// standing between the log and the page. -func TestMarkdownDropsDangerousLinks(t *testing.T) { - m := testMarkdown(t) - for _, in := range []string{ - "[click](javascript:alert(1))", - ")", - "[click](vbscript:alert(1))", - } { - if got := string(m.render(in)); strings.Contains(got, "javascript:") || strings.Contains(got, "vbscript:") { - t.Errorf("%q rendered as %s", in, got) - } - } -} - -// A fenced block is painted by the same grammars as the code view, so a -// snippet in a note wears the reader's theme. -func TestMarkdownHighlightsFences(t *testing.T) { - m := testMarkdown(t) - got := string(m.render("```go\nfunc load() error { return nil }\n```")) - for _, w := range []string{`<pre class="mdcode" data-lang="go">`, `<span class="s-keyword">func</span>`, "</code></pre>"} { - if !strings.Contains(got, w) { - t.Errorf("missing %q in:\n%s", w, got) - } - } - // a language no grammar claims still renders, just unpainted - got = string(m.render("```brainfuck\n+[-]<>\n```")) - if !strings.Contains(got, "+[-]<>") { - t.Errorf("unclaimed fence lost its text:\n%s", got) - } - if strings.Contains(got, "<span") { - t.Errorf("unclaimed fence was painted:\n%s", got) - } -} - -// Fence languages are mostly extensions already, so the grammar table does the -// lookup; fenceAliases only has to cover the spelled-out names. Each pair here -// is a snippet the claimed grammar must find something to paint in. -func TestMarkdownFenceLanguages(t *testing.T) { - hl := newHighlighter(loadTheme("", "", "dark"), loadTheme("", "", "light")) - for _, c := range []struct{ lang, src string }{ - {"go", "func f() {}"}, {"GO", "func f() {}"}, {"golang", "func f() {}"}, - {"rs", "fn f() {}"}, {"rust", "fn f() {}"}, - {"py", "def f(): pass"}, {"python", "def f(): pass"}, {"python3", "def f(): pass"}, - {"js", "function f() {}"}, {"javascript", "function f() {}"}, {"node", "function f() {}"}, - {"jsx", "function f() {}"}, {"mjs", "function f() {}"}, - {"ts", "function f(): void {}"}, {"typescript", "function f(): void {}"}, - {"tsx", "function f(): void {}"}, - {"c", "int f(void) { return 0; }"}, - {"cpp", "int f() { return 0; }"}, {"c++", "int f() { return 0; }"}, {"h", "int f();"}, - {"zig", "fn f() void {}"}, - {"json", `{"a": 1}`}, {"jsonc", `{"a": 1}`}, {"json5", `{"a": 1}`}, - {"sh", "for x in a; do echo $x; done"}, {"bash", "for x in a; do echo $x; done"}, - {"shell", "for x in a; do echo $x; done"}, {"zsh", "for x in a; do echo $x; done"}, - {"console", "for x in a; do echo $x; done"}, - } { - if !painted(hl.classifyLang(c.lang, []byte(c.src))) { - t.Errorf("fence language %q painted nothing in %q", c.lang, c.src) - } - } - for _, lang := range []string{"", "text", "brainfuck", " ", "."} { - src := []byte("func f() {}") - if got := hl.classifyLang(lang, src); len(got) != len(src) || painted(got) { - t.Errorf("classifyLang(%q) claimed a grammar it should not have", lang) - } - } -} - -func painted(classes []int16) bool { - for _, c := range classes { - if c != 0 { - return true - } - } - return false -} - -// The note card is the one place all of this has to arrive: the notes page, -// the aside on a file, and the conversation view all render it. -func TestNotePageRendersMarkdown(t *testing.T) { - sv, _, _ := testServer(t) - h := sv.handler() - - form := url.Values{"file": {"hello.txt"}, "text": { - "`cover()` widens the **mask**\n\n- one\n- two\n\n```go\nfunc f() {}\n```"}, "rev": {"@"}} - r := httptest.NewRequest("POST", "/notes", strings.NewReader(form.Encode())) - r.Header.Set("Content-Type", "application/x-www-form-urlencoded") - r.Header.Set("HX-Request", "true") - w := httptest.NewRecorder() - h.ServeHTTP(w, r) - if w.Code != 200 { - t.Fatalf("POST /notes = %d: %s", w.Code, w.Body) - } - for _, want := range []string{ - `<div class="body">`, - "<code>cover()</code>", "<strong>mask</strong>", - "<li>one</li>", - `<pre class="mdcode" data-lang="go">`, `<span class="s-keyword">func</span>`, - } { - if !strings.Contains(w.Body.String(), want) { - t.Errorf("missing %q in the posted note card:\n%s", want, w.Body) - } - } - // and again off the page itself, not just the htmx fragment - if b := get(t, h, "/notes", false).Body.String(); !strings.Contains(b, "<code>cover()</code>") { - t.Error("notes page did not render the note as markdown") - } -} - -func TestMarkdownEmpty(t *testing.T) { - m := testMarkdown(t) - for _, in := range []string{"", " ", "\n\n"} { - if got := m.render(in); got != "" { - t.Errorf("render(%q) = %q, want empty", in, got) - } - } -} |
