summaryrefslogtreecommitdiff
path: root/vrsite/markdown.go
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-02 23:43:08 -0300
committerGabriel Schneider <[email protected]>2026-08-03 09:54:39 -0300
commit5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e (patch)
treef12f4b700fff4ff6f7e818d2a542672ba0b8d76e /vrsite/markdown.go
parenta9263daee9413c5eff3c1f1bebcd224442fd8685 (diff)
downloadnotevi-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.go259
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(&noteRenderer{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")
-}