opencode-replay 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/assets/highlight.js +300 -0
- package/dist/assets/prism.css +273 -0
- package/dist/assets/search.js +445 -0
- package/dist/assets/styles.css +3384 -0
- package/dist/assets/theme.js +111 -0
- package/dist/index.js +2569 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ramtin Javanmardi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# opencode-replay
|
|
2
|
+
|
|
3
|
+
A CLI tool that generates static HTML transcripts from [OpenCode](https://github.com/sst/opencode) sessions, enabling browsing, searching, and sharing of AI-assisted coding conversations.
|
|
4
|
+
|
|
5
|
+
## Why?
|
|
6
|
+
|
|
7
|
+
OpenCode stores session data in `~/.local/share/opencode/storage/` as JSON files, but this data isn't easily browsable or shareable. `opencode-replay` transforms these sessions into clean, searchable, static HTML pages.
|
|
8
|
+
|
|
9
|
+
**Use cases:**
|
|
10
|
+
- **PR Documentation** - Attach session transcripts to pull requests showing the AI collaboration process
|
|
11
|
+
- **Self-Review** - Analyze past sessions to identify effective prompting patterns
|
|
12
|
+
- **Team Sharing** - Share session transcripts with teammates for knowledge transfer
|
|
13
|
+
- **Debugging** - Review what happened in a session when something went wrong
|
|
14
|
+
- **Compliance** - Maintain audit trails of AI-assisted code generation
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Using bun (recommended)
|
|
20
|
+
bun install -g opencode-replay
|
|
21
|
+
|
|
22
|
+
# Using npm
|
|
23
|
+
npm install -g opencode-replay
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Generate HTML for current project's sessions
|
|
30
|
+
cd /path/to/your/project
|
|
31
|
+
opencode-replay
|
|
32
|
+
|
|
33
|
+
# Open the generated transcript in your browser
|
|
34
|
+
opencode-replay --open
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Basic Commands
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Generate HTML for current project (auto-detects from cwd)
|
|
43
|
+
opencode-replay
|
|
44
|
+
|
|
45
|
+
# Generate HTML for ALL projects across your machine
|
|
46
|
+
opencode-replay --all
|
|
47
|
+
|
|
48
|
+
# Specify output directory (default: ./opencode-replay-output)
|
|
49
|
+
opencode-replay -o ./my-transcripts
|
|
50
|
+
|
|
51
|
+
# Export a specific session by ID
|
|
52
|
+
opencode-replay --session ses_4957d04cdffeJwdujYPBCKpIsb
|
|
53
|
+
|
|
54
|
+
# Open in browser after generation
|
|
55
|
+
opencode-replay --open
|
|
56
|
+
|
|
57
|
+
# Include raw JSON export alongside HTML
|
|
58
|
+
opencode-replay --json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### HTTP Server Mode
|
|
62
|
+
|
|
63
|
+
Serve generated transcripts via HTTP for easier viewing and sharing:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Generate and serve via HTTP (default port: 3000)
|
|
67
|
+
opencode-replay --serve
|
|
68
|
+
|
|
69
|
+
# Serve on a custom port
|
|
70
|
+
opencode-replay --serve --port 8080
|
|
71
|
+
|
|
72
|
+
# Serve existing output without regenerating
|
|
73
|
+
opencode-replay --serve --no-generate -o ./existing-output
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The built-in server includes:
|
|
77
|
+
- Automatic MIME type detection
|
|
78
|
+
- ETag-based caching for efficient reloads
|
|
79
|
+
- Directory index serving (serves `index.html` for directory requests)
|
|
80
|
+
- Path traversal protection
|
|
81
|
+
- Auto-opens browser on start
|
|
82
|
+
|
|
83
|
+
### All Options
|
|
84
|
+
|
|
85
|
+
| Option | Short | Description |
|
|
86
|
+
|--------|-------|-------------|
|
|
87
|
+
| `--all` | `-a` | Generate for all projects (default: current project only) |
|
|
88
|
+
| `--output <dir>` | `-o` | Output directory (default: `./opencode-replay-output`) |
|
|
89
|
+
| `--session <id>` | `-s` | Generate for a specific session only |
|
|
90
|
+
| `--json` | | Include raw JSON export alongside HTML |
|
|
91
|
+
| `--open` | | Open in browser after generation |
|
|
92
|
+
| `--storage <path>` | | Custom storage path (default: `~/.local/share/opencode/storage`) |
|
|
93
|
+
| `--serve` | | Start HTTP server after generation |
|
|
94
|
+
| `--port <number>` | | Server port (default: `3000`) |
|
|
95
|
+
| `--no-generate` | | Skip generation, only serve existing output |
|
|
96
|
+
| `--help` | `-h` | Show help message |
|
|
97
|
+
| `--version` | `-v` | Show version |
|
|
98
|
+
|
|
99
|
+
### Examples
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Generate transcripts for your current project
|
|
103
|
+
cd ~/workspace/my-project
|
|
104
|
+
opencode-replay
|
|
105
|
+
|
|
106
|
+
# Generate all sessions and open in browser
|
|
107
|
+
opencode-replay --all --open
|
|
108
|
+
|
|
109
|
+
# Export a specific session with JSON data
|
|
110
|
+
opencode-replay --session ses_abc123 --json -o ./session-export
|
|
111
|
+
|
|
112
|
+
# Use custom storage location
|
|
113
|
+
opencode-replay --storage /custom/path/to/storage
|
|
114
|
+
|
|
115
|
+
# Generate and serve for easy sharing
|
|
116
|
+
opencode-replay --serve --port 8080
|
|
117
|
+
|
|
118
|
+
# Quick preview of existing transcripts
|
|
119
|
+
opencode-replay --serve --no-generate -o ./my-transcripts
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Output Structure
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
opencode-replay-output/
|
|
126
|
+
├── index.html # Master index (all sessions)
|
|
127
|
+
├── assets/
|
|
128
|
+
│ ├── styles.css # Stylesheet
|
|
129
|
+
│ └── search.js # Client-side search (coming soon)
|
|
130
|
+
├── projects/ # Only in --all mode
|
|
131
|
+
│ └── {project-name}/
|
|
132
|
+
│ └── index.html # Project-level session list
|
|
133
|
+
└── sessions/
|
|
134
|
+
└── {session-id}/
|
|
135
|
+
├── index.html # Session overview with timeline
|
|
136
|
+
├── page-001.html # Conversation pages (5 prompts each)
|
|
137
|
+
├── page-002.html
|
|
138
|
+
└── session.json # Raw data (if --json flag)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Tool Renderers
|
|
142
|
+
|
|
143
|
+
Each tool type has specialized rendering:
|
|
144
|
+
|
|
145
|
+
| Tool | Display |
|
|
146
|
+
|------|---------|
|
|
147
|
+
| `bash` | Terminal-style command with `$` prefix, dark output box |
|
|
148
|
+
| `read` | File path header with content preview and line numbers |
|
|
149
|
+
| `write` | File path with "Created" badge, content preview |
|
|
150
|
+
| `edit` | Side-by-side diff view (old/new comparison) |
|
|
151
|
+
| `glob` | Pattern with file list and type icons |
|
|
152
|
+
| `grep` | Pattern with matching lines (file:line format) |
|
|
153
|
+
| `task` | Agent type badge with collapsible prompt/result |
|
|
154
|
+
| `todowrite` | Checklist with status icons |
|
|
155
|
+
| `webfetch` | Clickable URL with content preview |
|
|
156
|
+
| `batch` | Nested tool call summary |
|
|
157
|
+
|
|
158
|
+
## Requirements
|
|
159
|
+
|
|
160
|
+
- [Bun](https://bun.sh) runtime (recommended) or Node.js 18+
|
|
161
|
+
- OpenCode sessions in standard storage location
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# Clone the repository
|
|
167
|
+
git clone https://github.com/ramtinJ95/opencode-replay
|
|
168
|
+
cd opencode-replay
|
|
169
|
+
|
|
170
|
+
# Install dependencies
|
|
171
|
+
bun install
|
|
172
|
+
|
|
173
|
+
# Run in development mode
|
|
174
|
+
bun run src/index.ts
|
|
175
|
+
|
|
176
|
+
# Type check
|
|
177
|
+
bun run typecheck
|
|
178
|
+
|
|
179
|
+
# Build for distribution
|
|
180
|
+
bun run build
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Replay - Lightweight Syntax Highlighter
|
|
3
|
+
* A minimal syntax highlighter for common programming languages
|
|
4
|
+
* No external dependencies, works offline
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
(function() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// Language definitions with token patterns
|
|
11
|
+
const languages = {
|
|
12
|
+
// JavaScript/TypeScript
|
|
13
|
+
javascript: {
|
|
14
|
+
patterns: [
|
|
15
|
+
{ type: 'comment', pattern: /\/\/.*$/gm },
|
|
16
|
+
{ type: 'comment', pattern: /\/\*[\s\S]*?\*\//g },
|
|
17
|
+
{ type: 'string', pattern: /(["'`])(?:(?!\1)[^\\]|\\.)*\1/g },
|
|
18
|
+
{ type: 'keyword', pattern: /\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|try|catch|finally|throw|new|class|extends|import|export|from|default|async|await|yield|typeof|instanceof|in|of|void|delete|this|super|static|get|set)\b/g },
|
|
19
|
+
{ type: 'boolean', pattern: /\b(true|false|null|undefined|NaN|Infinity)\b/g },
|
|
20
|
+
{ type: 'number', pattern: /\b\d+\.?\d*([eE][+-]?\d+)?\b/g },
|
|
21
|
+
{ type: 'function', pattern: /\b([a-zA-Z_$][\w$]*)\s*(?=\()/g },
|
|
22
|
+
{ type: 'class-name', pattern: /\b([A-Z][\w]*)\b/g },
|
|
23
|
+
{ type: 'operator', pattern: /[+\-*/%=<>!&|^~?:]+/g },
|
|
24
|
+
{ type: 'punctuation', pattern: /[{}[\]();,]/g }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
typescript: { extends: 'javascript' },
|
|
28
|
+
jsx: { extends: 'javascript' },
|
|
29
|
+
tsx: { extends: 'javascript' },
|
|
30
|
+
|
|
31
|
+
// Python
|
|
32
|
+
python: {
|
|
33
|
+
patterns: [
|
|
34
|
+
{ type: 'comment', pattern: /#.*$/gm },
|
|
35
|
+
{ type: 'string', pattern: /("""[\s\S]*?"""|'''[\s\S]*?''')/g },
|
|
36
|
+
{ type: 'string', pattern: /(["'])(?:(?!\1)[^\\]|\\.)*\1/g },
|
|
37
|
+
{ type: 'keyword', pattern: /\b(and|as|assert|async|await|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield|True|False|None)\b/g },
|
|
38
|
+
{ type: 'builtin', pattern: /\b(print|len|range|str|int|float|list|dict|set|tuple|bool|type|isinstance|hasattr|getattr|setattr|open|input|map|filter|reduce|zip|enumerate|sorted|reversed|sum|min|max|abs|round)\b/g },
|
|
39
|
+
{ type: 'number', pattern: /\b\d+\.?\d*([eE][+-]?\d+)?\b/g },
|
|
40
|
+
{ type: 'function', pattern: /\b([a-zA-Z_][\w]*)\s*(?=\()/g },
|
|
41
|
+
{ type: 'class-name', pattern: /\bclass\s+([A-Z][\w]*)/g },
|
|
42
|
+
{ type: 'decorator', pattern: /@[\w.]+/g },
|
|
43
|
+
{ type: 'operator', pattern: /[+\-*/%=<>!&|^~@]+/g },
|
|
44
|
+
{ type: 'punctuation', pattern: /[{}[\]();:,]/g }
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// JSON
|
|
49
|
+
json: {
|
|
50
|
+
patterns: [
|
|
51
|
+
{ type: 'property', pattern: /"[^"\\]*(?:\\.[^"\\]*)*"(?=\s*:)/g },
|
|
52
|
+
{ type: 'string', pattern: /"[^"\\]*(?:\\.[^"\\]*)*"/g },
|
|
53
|
+
{ type: 'number', pattern: /-?\b\d+\.?\d*([eE][+-]?\d+)?\b/g },
|
|
54
|
+
{ type: 'boolean', pattern: /\b(true|false|null)\b/g },
|
|
55
|
+
{ type: 'punctuation', pattern: /[{}[\]:,]/g }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// HTML/XML
|
|
60
|
+
html: {
|
|
61
|
+
patterns: [
|
|
62
|
+
{ type: 'comment', pattern: /<!--[\s\S]*?-->/g },
|
|
63
|
+
{ type: 'tag', pattern: /<\/?[\w-]+/g },
|
|
64
|
+
{ type: 'attr-name', pattern: /\s[\w-]+(?==)/g },
|
|
65
|
+
{ type: 'attr-value', pattern: /=\s*(["'])(?:(?!\1)[^\\]|\\.)*\1/g },
|
|
66
|
+
{ type: 'punctuation', pattern: /[<>\/=]/g }
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
xml: { extends: 'html' },
|
|
70
|
+
svg: { extends: 'html' },
|
|
71
|
+
|
|
72
|
+
// CSS
|
|
73
|
+
css: {
|
|
74
|
+
patterns: [
|
|
75
|
+
{ type: 'comment', pattern: /\/\*[\s\S]*?\*\//g },
|
|
76
|
+
{ type: 'selector', pattern: /[^{}]+(?=\s*\{)/g },
|
|
77
|
+
{ type: 'property', pattern: /[\w-]+(?=\s*:)/g },
|
|
78
|
+
{ type: 'string', pattern: /(["'])(?:(?!\1)[^\\]|\\.)*\1/g },
|
|
79
|
+
{ type: 'number', pattern: /-?\b\d+\.?\d*(px|em|rem|%|vh|vw|deg|s|ms)?\b/g },
|
|
80
|
+
{ type: 'function', pattern: /[\w-]+(?=\()/g },
|
|
81
|
+
{ type: 'punctuation', pattern: /[{}();:,]/g }
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
scss: { extends: 'css' },
|
|
85
|
+
less: { extends: 'css' },
|
|
86
|
+
|
|
87
|
+
// Bash/Shell
|
|
88
|
+
bash: {
|
|
89
|
+
patterns: [
|
|
90
|
+
{ type: 'comment', pattern: /#.*$/gm },
|
|
91
|
+
{ type: 'string', pattern: /(["'])(?:(?!\1)[^\\]|\\.)*\1/g },
|
|
92
|
+
{ type: 'keyword', pattern: /\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|exit|break|continue|export|source|alias|unalias|cd|pwd|echo|printf|read|test)\b/g },
|
|
93
|
+
{ type: 'builtin', pattern: /\b(ls|cat|grep|sed|awk|find|xargs|sort|uniq|head|tail|wc|cut|tr|mkdir|rm|cp|mv|chmod|chown|curl|wget|tar|gzip|gunzip|ssh|scp|git|npm|yarn|bun|node|python|pip)\b/g },
|
|
94
|
+
{ type: 'variable', pattern: /\$[\w]+|\$\{[^}]+\}/g },
|
|
95
|
+
{ type: 'operator', pattern: /[|&;><]+/g },
|
|
96
|
+
{ type: 'punctuation', pattern: /[()[\]{}]/g }
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
sh: { extends: 'bash' },
|
|
100
|
+
shell: { extends: 'bash' },
|
|
101
|
+
zsh: { extends: 'bash' },
|
|
102
|
+
|
|
103
|
+
// SQL
|
|
104
|
+
sql: {
|
|
105
|
+
patterns: [
|
|
106
|
+
{ type: 'comment', pattern: /--.*$/gm },
|
|
107
|
+
{ type: 'comment', pattern: /\/\*[\s\S]*?\*\//g },
|
|
108
|
+
{ type: 'string', pattern: /(["'])(?:(?!\1)[^\\]|\\.)*\1/g },
|
|
109
|
+
{ type: 'keyword', pattern: /\b(SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|AND|OR|NOT|IN|EXISTS|BETWEEN|LIKE|IS|NULL|AS|ORDER|BY|GROUP|HAVING|LIMIT|OFFSET|INSERT|INTO|VALUES|UPDATE|SET|DELETE|CREATE|TABLE|INDEX|VIEW|DROP|ALTER|ADD|COLUMN|PRIMARY|KEY|FOREIGN|REFERENCES|UNIQUE|DEFAULT|CHECK|CONSTRAINT|CASCADE|UNION|ALL|DISTINCT|COUNT|SUM|AVG|MIN|MAX|CASE|WHEN|THEN|ELSE|END)\b/gi },
|
|
110
|
+
{ type: 'number', pattern: /\b\d+\.?\d*\b/g },
|
|
111
|
+
{ type: 'operator', pattern: /[=<>!+\-*/%]+/g },
|
|
112
|
+
{ type: 'punctuation', pattern: /[(),;.]/g }
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Go
|
|
117
|
+
go: {
|
|
118
|
+
patterns: [
|
|
119
|
+
{ type: 'comment', pattern: /\/\/.*$/gm },
|
|
120
|
+
{ type: 'comment', pattern: /\/\*[\s\S]*?\*\//g },
|
|
121
|
+
{ type: 'string', pattern: /(["'`])(?:(?!\1)[^\\]|\\.)*\1/g },
|
|
122
|
+
{ type: 'keyword', pattern: /\b(break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/g },
|
|
123
|
+
{ type: 'builtin', pattern: /\b(append|cap|close|complex|copy|delete|imag|len|make|new|panic|print|println|real|recover)\b/g },
|
|
124
|
+
{ type: 'boolean', pattern: /\b(true|false|nil|iota)\b/g },
|
|
125
|
+
{ type: 'number', pattern: /\b\d+\.?\d*([eE][+-]?\d+)?\b/g },
|
|
126
|
+
{ type: 'function', pattern: /\b([a-zA-Z_][\w]*)\s*(?=\()/g },
|
|
127
|
+
{ type: 'class-name', pattern: /\b([A-Z][\w]*)\b/g },
|
|
128
|
+
{ type: 'operator', pattern: /[+\-*/%=<>!&|^:]+/g },
|
|
129
|
+
{ type: 'punctuation', pattern: /[{}[\]();,]/g }
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// Rust
|
|
134
|
+
rust: {
|
|
135
|
+
patterns: [
|
|
136
|
+
{ type: 'comment', pattern: /\/\/.*$/gm },
|
|
137
|
+
{ type: 'comment', pattern: /\/\*[\s\S]*?\*\//g },
|
|
138
|
+
{ type: 'string', pattern: /(["'])(?:(?!\1)[^\\]|\\.)*\1/g },
|
|
139
|
+
{ type: 'keyword', pattern: /\b(as|async|await|break|const|continue|crate|dyn|else|enum|extern|false|fn|for|if|impl|in|let|loop|match|mod|move|mut|pub|ref|return|self|Self|static|struct|super|trait|true|type|unsafe|use|where|while)\b/g },
|
|
140
|
+
{ type: 'builtin', pattern: /\b(Option|Result|Some|None|Ok|Err|Vec|String|Box|Rc|Arc|Cell|RefCell|HashMap|HashSet|println!|print!|format!|vec!|panic!|assert!|debug!)\b/g },
|
|
141
|
+
{ type: 'number', pattern: /\b\d+\.?\d*([eE][+-]?\d+)?[iu]?(8|16|32|64|128|size)?\b/g },
|
|
142
|
+
{ type: 'function', pattern: /\b([a-z_][\w]*)\s*(?=\()/g },
|
|
143
|
+
{ type: 'class-name', pattern: /\b([A-Z][\w]*)\b/g },
|
|
144
|
+
{ type: 'lifetime', pattern: /'[a-z_][\w]*/g },
|
|
145
|
+
{ type: 'operator', pattern: /[+\-*/%=<>!&|^:?]+/g },
|
|
146
|
+
{ type: 'punctuation', pattern: /[{}[\]();,]/g }
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// YAML
|
|
151
|
+
yaml: {
|
|
152
|
+
patterns: [
|
|
153
|
+
{ type: 'comment', pattern: /#.*$/gm },
|
|
154
|
+
{ type: 'property', pattern: /^[\w-]+(?=\s*:)/gm },
|
|
155
|
+
{ type: 'string', pattern: /(["'])(?:(?!\1)[^\\]|\\.)*\1/g },
|
|
156
|
+
{ type: 'boolean', pattern: /\b(true|false|yes|no|on|off|null|~)\b/gi },
|
|
157
|
+
{ type: 'number', pattern: /\b\d+\.?\d*\b/g },
|
|
158
|
+
{ type: 'punctuation', pattern: /[:\-[\]{}|>]/g }
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
yml: { extends: 'yaml' },
|
|
162
|
+
|
|
163
|
+
// Markdown
|
|
164
|
+
markdown: {
|
|
165
|
+
patterns: [
|
|
166
|
+
{ type: 'title', pattern: /^#{1,6}\s+.+$/gm },
|
|
167
|
+
{ type: 'bold', pattern: /\*\*[^*]+\*\*|__[^_]+__/g },
|
|
168
|
+
{ type: 'italic', pattern: /\*[^*]+\*|_[^_]+_/g },
|
|
169
|
+
{ type: 'code', pattern: /`[^`]+`/g },
|
|
170
|
+
{ type: 'url', pattern: /\[[^\]]+\]\([^)]+\)/g },
|
|
171
|
+
{ type: 'list', pattern: /^[\s]*[-*+]\s/gm },
|
|
172
|
+
{ type: 'blockquote', pattern: /^>\s.+$/gm }
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
md: { extends: 'markdown' },
|
|
176
|
+
|
|
177
|
+
// Diff
|
|
178
|
+
diff: {
|
|
179
|
+
patterns: [
|
|
180
|
+
{ type: 'deleted', pattern: /^-.*$/gm },
|
|
181
|
+
{ type: 'inserted', pattern: /^\+.*$/gm },
|
|
182
|
+
{ type: 'coord', pattern: /^@@.*@@$/gm },
|
|
183
|
+
{ type: 'comment', pattern: /^(diff|index|---|\+\+\+).*$/gm }
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// Plain text (no highlighting)
|
|
188
|
+
text: { patterns: [] },
|
|
189
|
+
plaintext: { patterns: [] }
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Resolve language extensions
|
|
193
|
+
function getLanguagePatterns(lang) {
|
|
194
|
+
const langDef = languages[lang] || languages.text;
|
|
195
|
+
if (langDef.extends) {
|
|
196
|
+
return languages[langDef.extends]?.patterns || [];
|
|
197
|
+
}
|
|
198
|
+
return langDef.patterns || [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Escape HTML special characters
|
|
202
|
+
function escapeHtml(str) {
|
|
203
|
+
return str
|
|
204
|
+
.replace(/&/g, '&')
|
|
205
|
+
.replace(/</g, '<')
|
|
206
|
+
.replace(/>/g, '>')
|
|
207
|
+
.replace(/"/g, '"')
|
|
208
|
+
.replace(/'/g, ''');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Highlight code with given language
|
|
212
|
+
function highlight(code, lang) {
|
|
213
|
+
const normalizedLang = (lang || 'text').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
214
|
+
const patterns = getLanguagePatterns(normalizedLang);
|
|
215
|
+
|
|
216
|
+
if (patterns.length === 0) {
|
|
217
|
+
return escapeHtml(code);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Track which parts of the code have been tokenized
|
|
221
|
+
const tokens = [];
|
|
222
|
+
|
|
223
|
+
// Apply each pattern
|
|
224
|
+
for (const { type, pattern } of patterns) {
|
|
225
|
+
// Reset pattern lastIndex
|
|
226
|
+
pattern.lastIndex = 0;
|
|
227
|
+
let match;
|
|
228
|
+
|
|
229
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
230
|
+
tokens.push({
|
|
231
|
+
type,
|
|
232
|
+
start: match.index,
|
|
233
|
+
end: match.index + match[0].length,
|
|
234
|
+
text: match[0]
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sort tokens by start position
|
|
240
|
+
tokens.sort((a, b) => a.start - b.start);
|
|
241
|
+
|
|
242
|
+
// Remove overlapping tokens (keep first match)
|
|
243
|
+
const filtered = [];
|
|
244
|
+
let lastEnd = 0;
|
|
245
|
+
for (const token of tokens) {
|
|
246
|
+
if (token.start >= lastEnd) {
|
|
247
|
+
filtered.push(token);
|
|
248
|
+
lastEnd = token.end;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Build highlighted HTML
|
|
253
|
+
let result = '';
|
|
254
|
+
let pos = 0;
|
|
255
|
+
for (const token of filtered) {
|
|
256
|
+
if (token.start > pos) {
|
|
257
|
+
result += escapeHtml(code.slice(pos, token.start));
|
|
258
|
+
}
|
|
259
|
+
result += `<span class="token ${token.type}">${escapeHtml(token.text)}</span>`;
|
|
260
|
+
pos = token.end;
|
|
261
|
+
}
|
|
262
|
+
if (pos < code.length) {
|
|
263
|
+
result += escapeHtml(code.slice(pos));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Find and highlight all code blocks on page load
|
|
270
|
+
function highlightAll() {
|
|
271
|
+
const codeBlocks = document.querySelectorAll('pre code[class*="language-"]');
|
|
272
|
+
|
|
273
|
+
for (const block of codeBlocks) {
|
|
274
|
+
// Extract language from class
|
|
275
|
+
const classes = block.className.split(/\s+/);
|
|
276
|
+
const langClass = classes.find(c => c.startsWith('language-'));
|
|
277
|
+
const lang = langClass ? langClass.replace('language-', '') : 'text';
|
|
278
|
+
|
|
279
|
+
// Get original code text
|
|
280
|
+
const code = block.textContent || '';
|
|
281
|
+
|
|
282
|
+
// Apply highlighting
|
|
283
|
+
block.innerHTML = highlight(code, lang);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Run on DOM ready
|
|
288
|
+
if (document.readyState === 'loading') {
|
|
289
|
+
document.addEventListener('DOMContentLoaded', highlightAll);
|
|
290
|
+
} else {
|
|
291
|
+
highlightAll();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Expose API for dynamic use
|
|
295
|
+
window.OpenCodeHighlight = {
|
|
296
|
+
highlight,
|
|
297
|
+
highlightAll,
|
|
298
|
+
languages: Object.keys(languages)
|
|
299
|
+
};
|
|
300
|
+
})();
|