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.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.go')
| -rw-r--r-- | vrsite/markdown.go | 259 |
1 files changed, 0 insertions, 259 deletions
diff --git a/vrsite/markdown.go b/vrsite/markdown.go deleted file mode 100644 index 31581ae..0000000 --- a/vrsite/markdown.go +++ /dev/null @@ -1,259 +0,0 @@ -// markdown.go — note text is markdown. Agents write prose about code: -// backticked identifiers, bullet lists, the occasional fenced snippet, and -// links out. Rendering it as one escaped <p> threw all of that away. -// -// Two things make markdown fit text nobody wrote for a browser: -// -// - angle brackets are never markup. In a note they are code — -// RwLock<SysvarCache> is a type — so the HTML parsers come out and the -// brackets survive as the characters they were typed as. -// - fenced code goes through the same tree-sitter highlighter as the code -// view, so a snippet in a note is painted by the reader's theme exactly -// like the file it was copied from. -package main - -import ( - "bytes" - stdhtml "html" - "html/template" - "net/url" - "path" - "regexp" - "strconv" - "strings" - - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/extension" - mdparser "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/renderer" - "github.com/yuin/goldmark/renderer/html" - mdtext "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/util" -) - -// markdown renders note text. goldmark is safe for concurrent use, which the -// server (a goroutine per request) and the export (a goroutine per file) both -// rely on. -type markdown struct{ md goldmark.Markdown } - -// newMarkdown builds the note renderer. GFM adds the things people actually -// type — tables, ~~strikethrough~~, bare URLs, task lists — and hard wraps -// keep a newline a newline, since a note is typed into a textarea or passed on -// a command line, not authored as a document. -func newMarkdown(hl *highlighter) *markdown { - md := goldmark.New( - goldmark.WithParser(notesParser()), - goldmark.WithExtensions(extension.GFM), - goldmark.WithRendererOptions(html.WithHardWraps()), - ) - // lower priority number wins: this one replaces the stock code-block - // renderers, everything else stays goldmark's - md.Renderer().AddOptions(renderer.WithNodeRenderers( - util.Prioritized(¬eRenderer{hl: hl}, 100))) - return &markdown{md: md} -} - -// goldmark's priorities for the two parsers that read angle brackets as -// markup, from mdparser.DefaultBlockParsers and mdparser.DefaultInlineParsers. -const ( - htmlBlockPriority = 900 - rawHTMLPriority = 400 -) - -// notesParser is goldmark's default parser with those two dropped. Leaving -// them in costs the note either way: goldmark's safe default deletes raw HTML -// outright, so "RwLock<SysvarCache>" loses a word, and the block form -// additionally stops parsing markdown for the rest of the paragraph — a note -// that opens with a quoted <div> would render its links and lists as source. -// Without the parsers a bracket is just a character the text renderer escapes, -// which is both what the writer meant and the one thing a browser cannot act -// on. Dropping by priority keeps every other default, including any upstream -// adds later. -func notesParser() mdparser.Parser { - return mdparser.NewParser( - mdparser.WithBlockParsers(without(mdparser.DefaultBlockParsers(), htmlBlockPriority)...), - mdparser.WithInlineParsers(without(mdparser.DefaultInlineParsers(), rawHTMLPriority)...), - mdparser.WithParagraphTransformers(mdparser.DefaultParagraphTransformers()...), - ) -} - -func without(vs []util.PrioritizedValue, priority int) []util.PrioritizedValue { - out := vs[:0] - for _, v := range vs { - if v.Priority != priority { - out = append(out, v) - } - } - return out -} - -// newNoteLinkParser is shared across one model build. It uses the same parser -// and GFM extensions as rendered note bodies, so only links the reader can -// actually click become mentions (code spans and plain prose do not). -func newNoteLinkParser() mdparser.Parser { - return goldmark.New( - goldmark.WithParser(notesParser()), - goldmark.WithExtensions(extension.GFM), - ).Parser() -} - -func noteLinks(p mdparser.Parser, source string) []int { - if !strings.Contains(source, "note") { - return nil - } - src := []byte(source) - doc := p.Parse(mdtext.NewReader(src)) - seen := map[int]bool{} - var ids []int - _ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering || n.Kind() != ast.KindLink { - return ast.WalkContinue, nil - } - id := noteLinkID(string(n.(*ast.Link).Destination)) - if id > 0 && !seen[id] { - seen[id] = true - ids = append(ids, id) - } - return ast.WalkContinue, nil - }) - return ids -} - -// noteLinkID recognizes local links ending in note/N. Leading /, ./ and ../ -// components are all allowed; schemes and protocol-relative hosts are not. -func noteLinkID(destination string) int { - u, err := url.Parse(strings.TrimSpace(destination)) - if err != nil || u.Scheme != "" || u.Host != "" { - return 0 - } - clean := path.Clean("/" + strings.TrimSuffix(u.Path, ".html")) - parts := strings.Split(strings.Trim(clean, "/"), "/") - if len(parts) < 2 || parts[len(parts)-2] != "note" { - return 0 - } - id, err := strconv.Atoi(parts[len(parts)-1]) - if err != nil || id < 1 { - return 0 - } - return id -} - -// render turns one note's text into HTML. A note is never worth losing to a -// renderer error, so a failure falls back to the text as written. -func (m *markdown) render(text string) template.HTML { - if strings.TrimSpace(text) == "" { - return "" - } - var b bytes.Buffer - b.Grow(len(text) + len(text)/2) - if err := m.md.Convert([]byte(text), &b); err != nil { - return template.HTML("<p>" + template.HTMLEscapeString(text) + "</p>") - } - return template.HTML(b.String()) -} - -var noteHrefRE = regexp.MustCompile(`href="([^"]+)"`) - -// renderAt canonicalizes note links for the page carrying the card. A note is -// shown at several URL depths (/notes, /code/x, /note/N, and static variants), -// so leaving its relative Markdown href untouched would make the mention work -// in only one of those places. -func (m *markdown) renderAt(text string, p *page) template.HTML { - body := m.render(text) - if body == "" || p == nil { - return body - } - out := noteHrefRE.ReplaceAllStringFunc(string(body), func(attr string) string { - destination := stdhtml.UnescapeString(attr[len(`href="`) : len(attr)-1]) - id := noteLinkID(destination) - if id == 0 { - return attr - } - if !p.Live && p.Site != nil && p.Site.NoteByID(id) == nil { - return attr - } - href := p.Href("note", strconv.Itoa(id)) - if href == "" { - return attr - } - return `href="` + template.HTMLEscapeString(href) + `"` - }) - return template.HTML(out) -} - -// noteRenderer overrides the two node kinds the stock renderer emits without -// syntax colors. Everything else it renders is fine as goldmark writes it. -type noteRenderer struct{ hl *highlighter } - -func (nr *noteRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { - reg.Register(ast.KindFencedCodeBlock, nr.renderFenced) - reg.Register(ast.KindCodeBlock, nr.renderIndented) -} - -func (nr *noteRenderer) renderFenced(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkSkipChildren, nil - } - n := node.(*ast.FencedCodeBlock) - nr.code(w, blockLines(src, n), string(n.Language(src))) - return ast.WalkSkipChildren, nil -} - -func (nr *noteRenderer) renderIndented(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { - if !entering { - return ast.WalkSkipChildren, nil - } - nr.code(w, blockLines(src, node), "") - return ast.WalkSkipChildren, nil -} - -// blockLines joins a block node's source lines. They are contiguous in the -// source, but only the segments are authoritative about where the block's -// indentation was stripped. -func blockLines(src []byte, n ast.Node) []byte { - lines := n.Lines() - var b bytes.Buffer - for i := 0; i < lines.Len(); i++ { - seg := lines.At(i) - b.Write(seg.Value(src)) - } - return bytes.TrimRight(b.Bytes(), "\n") -} - -// code writes one code block, syntax-painted when a grammar claims the fence's -// language and plain when none does. It emits the same <span class="s-…"> -// runs as the code view, so the stylesheet already knows how to color it; the -// <li>-per-line scaffolding does not come along, since a snippet has no line -// numbers, no coverage and no notes of its own. -func (nr *noteRenderer) code(w util.BufWriter, src []byte, lang string) { - w.WriteString(`<pre class="mdcode"`) - if lang != "" { - w.WriteString(` data-lang="`) - w.WriteString(template.HTMLEscapeString(lang)) - w.WriteByte('"') - } - w.WriteString("><code>") - - var b strings.Builder - b.Grow(len(src) + len(src)/4) - classes := nr.hl.classifyLang(lang, src) - names := []string(nil) - if nr.hl != nil { - names = nr.hl.classNames - } - for start := 0; start <= len(src); { - end := start - for end < len(src) && src[end] != '\n' { - end++ - } - emitLine(&b, src[start:end], classes[start:end], names) - if end >= len(src) { - break - } - b.WriteByte('\n') - start = end + 1 - } - w.WriteString(b.String()) - w.WriteString("</code></pre>\n") -} |
