Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.26"

- name: Check formatting
run: |
diff=$(gofmt -l .)
if [ -n "$diff" ]; then
echo "Files not formatted:"
echo "$diff"
exit 1
fi

- name: Vet
run: go vet ./...

- name: Test
run: go test ./... -v
41 changes: 18 additions & 23 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,40 @@ jobs:
matrix:
include:
- runner: ubuntu-latest
target: x86_64-unknown-linux-gnu
goos: linux
goarch: amd64
asset: live-markdown-linux-x64
- runner: ubuntu-latest
target: aarch64-unknown-linux-gnu
goos: linux
goarch: arm64
asset: live-markdown-linux-arm64
- runner: macos-latest
target: x86_64-apple-darwin
goos: darwin
goarch: amd64
asset: live-markdown-darwin-x64
- runner: macos-latest
target: aarch64-apple-darwin
goos: darwin
goarch: arm64
asset: live-markdown-darwin-arm64
- runner: ubuntu-latest
target: x86_64-pc-windows-msvc
goos: windows
goarch: amd64
asset: live-markdown-windows-x64.exe

runs-on: ${{ matrix.runner }}

steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4

- uses: denoland/setup-deno@v2
- uses: actions/setup-go@v5
with:
deno-version: v2.x

- name: Download vendored assets
working-directory: server
run: deno task setup
go-version: "1.26"

- name: Compile binary
working-directory: server
run: >
deno compile
--allow-net=localhost
--allow-read
--include ../client/
--target ${{ matrix.target }}
--output ../bin/${{ matrix.asset }}
src/main.ts
- name: Build binary
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: go build -ldflags="-s -w" -o bin/${{ matrix.asset }} ./cmd/live-markdown

- name: Upload release asset
uses: softprops/action-gh-release@v2
Expand All @@ -68,4 +64,3 @@ jobs:
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true

14 changes: 9 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Build output
bin/

# Vendored assets (downloaded at build time)
client/vendor/

# Dependencies
node_modules/
# Go
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
vendor/

# OS
.DS_Store
Expand Down
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
golang 1.26.1
36 changes: 27 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ Read before implementing:
## Tech Stack

- **Neovim plugin**: Lua (no external dependencies)
- **Preview server**: Deno (TypeScript)
- **Markdown parser**: markdown-it (server-side rendering)
- **CSS**: github-markdown-css (loaded from CDN)
- **Diagrams**: mermaid.js (downloaded at build time from CDN, rendered client-side)
- **Preview server**: Go (single binary, `go:embed` for all assets)
- **Markdown parser**: goldmark + GFM extension (server-side rendering)
- **Syntax highlighting**: chroma (server-side, CSS class-based)
- **Math rendering**: KaTeX (client-side via auto-render)
- **CSS**: github-markdown-css (bundled in `static/`)
- **Diagrams**: mermaid.js (bundled in `static/`, rendered client-side)
- **Communication**: Neovim <-> Server via stdin/stdout (JSON Lines), Server <-> Browser via WebSocket
- **Distribution**: `deno compile` single binary -> GitHub Releases (users do not need Deno)
- **Distribution**: Go cross-compile single binary -> GitHub Releases

## Implementation Rules

Expand All @@ -72,10 +74,26 @@ Read before implementing:

- **Neovim <-> Server communication**: stdin/stdout JSON Lines (not WebSocket)
- **mermaid rendering**: Client-side (`mermaid.run()` in the browser, not server-side)
- **mermaid distribution**: Downloaded from CDN at build time to `client/vendor/mermaid.min.js`, served as `/vendor/mermaid.min.js`
- **github-markdown-css**: Loaded from CDN (cdnjs), not bundled
- **Scroll sync**: `data-source-line` attribute + `scrollIntoView()`
- **Fence rule**: Special handling to inject `data-source-line` on `<pre>` tags while preserving class attributes
- **KaTeX rendering**: Client-side (`renderMathInElement()` in the browser)
- **Static assets**: All bundled in `static/` and committed to git (no CDN, no build-time downloads)
- **Scroll sync**: `data-source-line` attribute via goldmark AST transformer + `scrollIntoView()`
- **Image paths**: Server rewrites relative src to `/_local/<absolute-path>`, served from local filesystem
- **Browser launch**: Presets + arbitrary command strings executed directly
- **Initial connection**: Server caches last rendered HTML, sends immediately on WebSocket connect
- **Server shutdown**: Sends `close` message to browser, attempts `window.close()`

## Project Structure

```
cmd/live-markdown/main.go # Go server entry point
internal/ # Go server internals
message/types.go # JSON Lines message types
jsonlines/ # stdin/stdout reader/writer
markdown/renderer.go # goldmark pipeline + source-line + image rewrite
server/server.go # HTTP + WebSocket server
assets.go # go:embed directives
static/ # Bundled assets (CSS, fonts, JS)
client/ # Browser client (index.html, preview.js)
lua/live-markdown/ # Neovim plugin (Lua)
scripts/install.sh # Binary installer from GitHub Releases
```
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

## Features

- Single binary — no runtime dependencies (Deno not required)
- Single binary — no runtime dependencies
- Real-time preview with scroll sync
- Syntax highlighting (light/dark auto-switch)
- Math rendering (KaTeX)
Expand Down Expand Up @@ -72,7 +72,7 @@ require("live-markdown").setup({
server = {
port = 0, -- 0 = OS auto-assigns
host = "localhost",
binary = nil, -- path to compiled binary (nil = use deno run)
binary = nil, -- path to compiled binary (nil = auto-detect bin/live-markdown)
},
browser = {
strategy = "auto", -- "auto" | "open" | "xdg-open" | custom command
Expand Down
9 changes: 9 additions & 0 deletions assets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package livemarkdown

import "embed"

//go:embed client/*
var ClientFS embed.FS

//go:embed static/css/* static/fonts/* static/js/katex.min.js static/js/mermaid.min.js static/js/contrib/auto-render.min.js
var StaticFS embed.FS
14 changes: 8 additions & 6 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
<title>live-markdown preview</title>

<!-- github-markdown-css: auto light/dark theme -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.8.1/github-markdown.min.css">
<link rel="stylesheet" href="/css/github-markdown.min.css">

<!-- highlight.js: syntax highlighting (light/dark auto-switch) -->
<link rel="stylesheet" media="(prefers-color-scheme: light)" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css">
<link rel="stylesheet" media="(prefers-color-scheme: dark)" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
<!-- syntax highlighting: light/dark auto-switch -->
<link rel="stylesheet" media="(prefers-color-scheme: light)" href="/css/chroma-github.css">
<link rel="stylesheet" media="(prefers-color-scheme: dark)" href="/css/chroma-github-dark.css">

<!-- KaTeX: math rendering -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.43/dist/katex.min.css">
<link rel="stylesheet" href="/css/katex.min.css">

<style>
body {
Expand All @@ -39,8 +39,10 @@
</head>
<body class="markdown-body">
<div id="content"><p class="connecting">Connecting...</p></div>
<script src="/vendor/mermaid.min.js"></script>
<script src="/js/mermaid.min.js"></script>
<script>mermaid.initialize({ startOnLoad: false, theme: "default" });</script>
<script src="/js/katex.min.js"></script>
<script src="/js/contrib/auto-render.min.js"></script>
<script src="/preview.js"></script>
</body>
</html>
15 changes: 15 additions & 0 deletions client/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@
let reconnectDelay = 1000;
let closed = false; // explicitly closed by server

// --- KaTeX math rendering ---

function renderMath() {
if (typeof renderMathInElement === "function") {
renderMathInElement(contentEl, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false },
],
throwOnError: false,
});
}
}

// --- Mermaid rendering ---

let mermaidCounter = 0;
Expand Down Expand Up @@ -90,6 +104,7 @@
switch (msg.type) {
case "render":
contentEl.innerHTML = msg.html;
renderMath();
renderMermaidBlocks();
break;
case "scroll":
Expand Down
42 changes: 42 additions & 0 deletions cmd/live-markdown/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"context"
"os"

livemarkdown "github.com/bun913/live-markdown.nvim"
"github.com/bun913/live-markdown.nvim/internal/jsonlines"
"github.com/bun913/live-markdown.nvim/internal/message"
"github.com/bun913/live-markdown.nvim/internal/server"
)

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

nvimOut := jsonlines.NewWriter(os.Stdout)

srv := server.New(livemarkdown.ClientFS, livemarkdown.StaticFS, nvimOut)

port, err := srv.ListenAndServe(ctx)
if err != nil {
os.Exit(1)
}

// Notify Neovim of the assigned port
nvimOut.Write(message.ServerMessage{Type: "ready", Port: port})

// Read stdin (JSON Lines) — exits on EOF (defense line 2)
msgCh := make(chan message.NvimMessage, 16)
go jsonlines.ReadStdin(os.Stdin, msgCh)

for msg := range msgCh {
if srv.HandleNvimMessage(msg) {
break
}
}

// stdin closed or close message received — shut down
srv.Shutdown()
os.Exit(0)
}
4 changes: 2 additions & 2 deletions docs/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

## 特徴

- シングルバイナリ — ランタイム依存なし(Deno のインストール不要)
- シングルバイナリ — ランタイム依存なし
- リアルタイムプレビュー + スクロール同期
- シンタックスハイライト(ライト/ダーク自動切替)
- 数式レンダリング(KaTeX)
Expand Down Expand Up @@ -70,7 +70,7 @@ require("live-markdown").setup({
server = {
port = 0, -- 0 = OS が自動割り当て
host = "localhost",
binary = nil, -- コンパイル済みバイナリのパス(nil = deno run を使用
binary = nil, -- コンパイル済みバイナリのパス(nil = bin/live-markdown を自動検出
},
browser = {
strategy = "auto", -- "auto" | "open" | "xdg-open" | 任意のコマンド
Expand Down
Loading
Loading