@rip-lang/print 0.1.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/README.md ADDED
@@ -0,0 +1,65 @@
1
+ <img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.png" style="width:50px" /> <br>
2
+
3
+ # Rip Print - @rip-lang/print
4
+
5
+ > **Syntax-highlighted source code printer**
6
+
7
+ Highlights source code using highlight.js and serves the result in the browser for viewing and printing. Serves once and exits — no leftover files, no cleanup.
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ # Install
13
+ bun add -g @rip-lang/print
14
+
15
+ # Print all source files in a directory
16
+ rip-print src/
17
+
18
+ # Print specific files
19
+ rip-print src/compiler.js src/lexer.js
20
+
21
+ # Dark theme
22
+ rip-print -d src/
23
+
24
+ # Strip leading comment blocks
25
+ rip-print -b src/
26
+
27
+ # Exclude extensions
28
+ rip-print -x lock,map src/
29
+ ```
30
+
31
+ ## Features
32
+
33
+ - **highlight.js** — 190+ languages, beautiful GitHub-style themes
34
+ - **Auto-detect languages** — 40+ languages supported
35
+ - **Light and dark themes** — GitHub Light (default) and GitHub Dark
36
+ - **Print-optimized CSS** — page breaks between files, clean headers
37
+ - **Table of contents** — auto-generated for multi-file output
38
+ - **Sticky headers** — file names stay visible while scrolling
39
+ - **Serve once** — opens browser, serves the page, exits automatically
40
+ - **No leftover files** — everything is in-memory
41
+
42
+ ## Options
43
+
44
+ | Flag | Description |
45
+ |------|-------------|
46
+ | `-b`, `--bypass` | Strip leading comment blocks from files |
47
+ | `-d`, `--dark` | Use dark theme (default: light) |
48
+ | `-h`, `--help` | Show help |
49
+ | `-x <exts>` | Comma list of extensions to exclude |
50
+
51
+ ## How It Works
52
+
53
+ 1. Walks the specified paths, discovers source files
54
+ 2. Highlights each file using Shiki with the detected language
55
+ 3. Builds a single HTML page with table of contents
56
+ 4. Starts a Bun server on a random port
57
+ 5. Opens the browser
58
+ 6. Exits after serving the page
59
+
60
+ Print from the browser with **Cmd+P** (Mac) or **Ctrl+P** (Windows/Linux).
61
+
62
+ ## Links
63
+
64
+ - [Rip Language](https://github.com/shreeve/rip-lang)
65
+ - [highlight.js](https://highlightjs.org)
package/bin/rip-print ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execFileSync } from 'child_process';
4
+ import { dirname, join } from 'path';
5
+
6
+ const printRip = join(dirname(new URL(import.meta.url).pathname), '..', 'print.rip');
7
+
8
+ try {
9
+ execFileSync('rip', [printRip, ...process.argv.slice(2)], { stdio: 'inherit' });
10
+ } catch (e) {
11
+ process.exit(e.status || 1);
12
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@rip-lang/print",
3
+ "version": "0.1.0",
4
+ "description": "Syntax-highlighted source code printer — Shiki-powered, serves once, auto-opens browser",
5
+ "type": "module",
6
+ "main": "print.rip",
7
+ "bin": {
8
+ "rip-print": "./bin/rip-print"
9
+ },
10
+ "keywords": [
11
+ "print",
12
+ "syntax",
13
+ "highlighting",
14
+ "highlight.js",
15
+ "source",
16
+ "code",
17
+ "bun",
18
+ "rip"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/shreeve/rip-lang.git",
23
+ "directory": "packages/print"
24
+ },
25
+ "homepage": "https://github.com/shreeve/rip-lang/tree/main/packages/print#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/shreeve/rip-lang/issues"
28
+ },
29
+ "author": "Steve Shreeve <steve.shreeve@gmail.com>",
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "rip-lang": "^3.4.4",
33
+ "highlight.js": "^11.11.1"
34
+ },
35
+ "files": [
36
+ "print.rip",
37
+ "bin/rip-print",
38
+ "README.md"
39
+ ]
40
+ }
package/print.rip ADDED
@@ -0,0 +1,365 @@
1
+ # ============================================================================
2
+ # rip-print — Syntax-highlighted source code printer
3
+ #
4
+ # Author: Steve Shreeve (steve.shreeve@gmail.com)
5
+ # Date: Feb 9, 2026
6
+ #
7
+ # Highlights source code using Shiki (VS Code's syntax engine) and serves
8
+ # the result in the browser for viewing and printing. Serves once and exits.
9
+ # ============================================================================
10
+
11
+ import hljs from 'highlight.js'
12
+ import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
13
+ import { basename, extname, join, relative } from 'path'
14
+ import { execSync } from 'child_process'
15
+
16
+ # ============================================================================
17
+ # CLI argument parsing
18
+ # ============================================================================
19
+
20
+ args = process.argv.slice(2)
21
+
22
+ if args.includes('-h') or args.includes('--help')
23
+ console.log """
24
+ usage: rip-print [options] <paths ...>
25
+
26
+ Options:
27
+ -b, --bypass Strip leading comment blocks from files
28
+ -d, --dark Use dark theme (default: light)
29
+ -h, --help Show this help message
30
+ -x <exts> Comma list of extensions to exclude
31
+
32
+ Examples:
33
+ rip-print src/ # Print all source files in src/
34
+ rip-print *.rip # Print all .rip files
35
+ rip-print -d src/ # Dark theme
36
+ rip-print -b src/ # Strip top comments
37
+ rip-print -x lock,map src/ # Exclude .lock and .map files
38
+ """
39
+ process.exit(0)
40
+
41
+ dark = args.includes('-d') or args.includes('--dark')
42
+ bypass = args.includes('-b') or args.includes('--bypass')
43
+
44
+ # Extract -x excludes
45
+ excludeExts = new Set()
46
+ for arg, i in args
47
+ if arg is '-x' and args[i + 1]
48
+ for ext in args[i + 1].split(',')
49
+ excludeExts.add ext.toLowerCase().replace(/^\./, '')
50
+
51
+ # Filter out flags, get paths
52
+ xIdx = args.indexOf('-x')
53
+ paths = args.filter (a, i) -> not a.startsWith('-') and (xIdx < 0 or i isnt xIdx + 1)
54
+ paths = ['.'] if paths.length is 0
55
+
56
+ # Default exclusions (binary, generated, etc.)
57
+ defaultExclude = new Set [
58
+ 'css', 'gif', 'ico', 'jpg', 'jpeg', 'png', 'svg', 'pdf', 'webp',
59
+ 'otf', 'ttf', 'eot', 'woff', 'woff2',
60
+ 'o', 'a', 'dylib', 'so', 'dll',
61
+ 'gem', 'gz', 'zip', 'tar', 'br',
62
+ 'lock', 'db', 'sqlite3', 'sqlite',
63
+ 'map', 'min.js', 'min.css',
64
+ 'vsix', 'DS_Store'
65
+ ]
66
+
67
+ for ext in excludeExts
68
+ defaultExclude.add ext
69
+
70
+ # ============================================================================
71
+ # File discovery
72
+ # ============================================================================
73
+
74
+ skipDirs = new Set ['.git', 'node_modules', '.rip-cache', '.zig-cache', 'zig-out', 'dist', 'misc']
75
+
76
+ def walkDir(dir, base = dir)
77
+ files = []
78
+ try
79
+ entries = readdirSync dir
80
+ catch
81
+ return files
82
+ for entry in entries
83
+ full = join dir, entry
84
+ try
85
+ stat = statSync full
86
+ catch
87
+ continue
88
+ if stat.isDirectory()
89
+ continue if entry.startsWith('.') or skipDirs.has(entry)
90
+ files = files.concat walkDir(full, base)
91
+ else if stat.isFile()
92
+ ext = entry.split('.').pop()?.toLowerCase() or ''
93
+ continue if defaultExclude.has(ext)
94
+ continue if entry.startsWith('.')
95
+ files.push relative(base, full) or full
96
+ files.sort()
97
+
98
+ allFiles = []
99
+ for p in paths
100
+ if not existsSync(p)
101
+ console.error "Not found: #{p}"
102
+ continue
103
+ stat = statSync p
104
+ if stat.isDirectory()
105
+ allFiles = allFiles.concat walkDir(p, p).map (f) -> join(p, f)
106
+ else if stat.isFile()
107
+ allFiles.push p
108
+
109
+ if allFiles.length is 0
110
+ console.error "No files found"
111
+ process.exit(1)
112
+
113
+ console.log "#{allFiles.length} file#{if allFiles.length > 1 then 's' else ''}"
114
+
115
+ # ============================================================================
116
+ # Language detection
117
+ # ============================================================================
118
+
119
+ extToLang =
120
+ rip: 'coffeescript'
121
+ coffee: 'coffeescript'
122
+ js: 'javascript'
123
+ mjs: 'javascript'
124
+ cjs: 'javascript'
125
+ ts: 'typescript'
126
+ mts: 'typescript'
127
+ cts: 'typescript'
128
+ jsx: 'jsx'
129
+ tsx: 'tsx'
130
+ rb: 'ruby'
131
+ py: 'python'
132
+ rs: 'rust'
133
+ go: 'go'
134
+ sh: 'bash'
135
+ bash: 'bash'
136
+ zsh: 'bash'
137
+ fish: 'fish'
138
+ yml: 'yaml'
139
+ yaml: 'yaml'
140
+ json: 'json'
141
+ jsonc: 'jsonc'
142
+ md: 'markdown'
143
+ html: 'html'
144
+ htm: 'html'
145
+ xml: 'xml'
146
+ css: 'css'
147
+ scss: 'scss'
148
+ sass: 'sass'
149
+ less: 'less'
150
+ sql: 'sql'
151
+ c: 'c'
152
+ h: 'c'
153
+ cpp: 'cpp'
154
+ hpp: 'cpp'
155
+ zig: 'zig'
156
+ toml: 'toml'
157
+ ini: 'ini'
158
+ dockerfile: 'dockerfile'
159
+ makefile: 'makefile'
160
+
161
+ def getLang(file)
162
+ base = basename(file).toLowerCase()
163
+ return 'makefile' if base is 'makefile' or base is 'gnumakefile'
164
+ return 'dockerfile' if base is 'dockerfile'
165
+ return 'yaml' if base is '.prettierrc' or base is '.eslintrc'
166
+ return 'json' if base is '.babelrc'
167
+ return 'bash' if base is '.bashrc' or base is '.zshrc' or base is '.profile'
168
+ return 'toml' if base is 'bunfig.toml'
169
+
170
+ ext = extname(file).slice(1).toLowerCase()
171
+ lang = extToLang[ext]
172
+ return lang if lang and hljs.getLanguage(lang)
173
+ return 'plaintext'
174
+
175
+ # ============================================================================
176
+ # Highlight and build HTML
177
+ # ============================================================================
178
+
179
+ def stripTopComments(code)
180
+ lines = code.split('\n')
181
+ i = 0
182
+ i++ while i < lines.length and (lines[i].startsWith('#') or lines[i].trim() is '')
183
+ return code if i is 0
184
+ lines.slice(i).join('\n')
185
+
186
+ def escapeHtml(str)
187
+ str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
188
+
189
+ def highlightCode(code, lang)
190
+ highlighted = null
191
+ try
192
+ if lang isnt 'plaintext'
193
+ result = hljs.highlight code, { language: lang }
194
+ highlighted = result.value
195
+ catch
196
+ # fall through
197
+ highlighted ?= escapeHtml code
198
+
199
+ # Add line numbers
200
+ lines = highlighted.split('\n')
201
+ width = String(lines.length).length
202
+ numbered = lines.map (line, i) ->
203
+ num = String(i + 1).padStart(width)
204
+ "<span class=\"line-num\">#{num}</span> #{line}"
205
+ numbered.join('\n')
206
+
207
+ sections = []
208
+ for file in allFiles
209
+ try
210
+ code = readFileSync file, 'utf-8'
211
+ catch
212
+ console.error " Skipping: #{file} (unreadable)"
213
+ continue
214
+ code = code.replace /\t/g, ' '
215
+ code = code.replace /\r\n?/g, '\n'
216
+ code = stripTopComments(code) if bypass
217
+ lang = getLang file
218
+ lineCount = code.split('\n').length
219
+ console.log " #{file} (#{lineCount} lines) [#{lang}]"
220
+ highlighted = highlightCode code, lang
221
+ html = "<pre><code class=\"hljs\">#{highlighted}</code></pre>"
222
+ sections.push { file, lineCount, lang, html }
223
+
224
+ # ============================================================================
225
+ # HTML template
226
+ # ============================================================================
227
+
228
+ bgColor = if dark then '#0d1117' else '#ffffff'
229
+ textColor = if dark then '#e6edf3' else '#1f2328'
230
+ headerBg = if dark then '#161b22' else '#f6f8fa'
231
+ borderColor = if dark then '#30363d' else '#d0d7de'
232
+
233
+ toc = ''
234
+ if sections.length > 1
235
+ toc = """
236
+ <div class="toc">
237
+ <h2>Files (#{sections.length})</h2>
238
+ <ol>
239
+ """
240
+ for section in sections
241
+ toc += """
242
+ <li><a href="##{section.file}">#{section.file}</a> <span class="meta">(#{section.lineCount} lines)</span></li>
243
+ """
244
+ toc += """
245
+ </ol>
246
+ </div>
247
+ """
248
+
249
+ fileSections = ''
250
+ count = sections.length
251
+ for section, i in sections
252
+ prev = sections[(i - 1 + count) % count]
253
+ next = sections[(i + 1) % count]
254
+ nav = ""
255
+ nav += "<a href=\"##{prev.file}\">prev</a> " if count > 1
256
+ nav += "<a href=\"##{next.file}\">next</a> " if count > 1
257
+ nav += "<a href=\"#top\">&uarr; top</a>"
258
+ fileSections += """
259
+ <div class="file-section">
260
+ <div class="file-header" id="#{section.file}">
261
+ <span>#{section.file} <span class="meta">(#{section.lineCount} lines) [#{section.lang}]</span></span>
262
+ <span class="nav">#{nav}</span>
263
+ </div>
264
+ <div class="code-container">
265
+ #{section.html}
266
+ </div>
267
+ </div>
268
+
269
+ """
270
+
271
+ # Read highlight.js CSS theme
272
+ hljsTheme = if dark then 'github-dark' else 'github'
273
+ hljsDir = join import.meta.dir, 'node_modules', 'highlight.js', 'styles'
274
+ hljsCss = readFileSync join(hljsDir, "#{hljsTheme}.css"), 'utf-8'
275
+
276
+ html = """
277
+ <!DOCTYPE html>
278
+ <html lang="en">
279
+ <head>
280
+ <meta charset="utf-8">
281
+ <title>rip-print</title>
282
+ <link rel="icon" href="data:,">
283
+ <style>#{hljsCss}</style>
284
+ <style>
285
+ * { margin: 0; padding: 0; box-sizing: border-box; }
286
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #{bgColor}; color: #{textColor}; }
287
+
288
+ .toc { padding: 20px 30px; border-bottom: 1px solid #{borderColor}; }
289
+ .toc h2 { font-size: 18px; margin-bottom: 10px; }
290
+ .toc ol { padding-left: 24px; columns: 2; column-gap: 40px; }
291
+ .toc li { font-size: 13px; line-height: 1.8; }
292
+ .toc a { color: #{textColor}; text-decoration: none; }
293
+ .toc a:hover { text-decoration: underline; }
294
+ .meta { color: #888; font-size: 12px; }
295
+
296
+ .file-section { margin-bottom: 0; }
297
+ .file-header {
298
+ background: #{headerBg}; padding: 8px 16px; font-size: 13px; font-weight: 600;
299
+ border-top: 1px solid #{borderColor}; border-bottom: 1px solid #{borderColor};
300
+ display: flex; justify-content: space-between; align-items: center;
301
+ }
302
+ .nav { font-weight: normal; font-size: 12px; }
303
+ .nav a { color: #888; text-decoration: none; margin-left: 12px; }
304
+ .nav a:hover { color: #{textColor}; }
305
+
306
+ .code-container { overflow-x: auto; }
307
+ .code-container pre { margin: 0; border-radius: 0; }
308
+ .code-container code { font-size: 13px; line-height: 1.5; padding: 16px !important; display: block; }
309
+ .line-num { color: #aaa; user-select: none; display: inline-block; min-width: 2em; text-align: right; }
310
+
311
+ @media print {
312
+ .toc { page-break-after: always; }
313
+ .file-header { background: #f0f0f0 !important; color: #000 !important; }
314
+ .nav { display: none; }
315
+ .file-section { page-break-before: always; }
316
+ .file-section:first-of-type { page-break-before: avoid; }
317
+ body { background: white !important; color: black !important; }
318
+ code { font-size: 11px !important; }
319
+ }
320
+ </style>
321
+ </head>
322
+ <body>
323
+ <a name="top"></a>
324
+ #{toc}
325
+ #{fileSections}
326
+ </body>
327
+ </html>
328
+ """
329
+
330
+ # ============================================================================
331
+ # Serve once and open browser
332
+ # ============================================================================
333
+
334
+ served = false
335
+
336
+ server = Bun.serve
337
+ port: 0
338
+ fetch: (req) ->
339
+ served = true
340
+ new Response html, headers: { 'Content-Type': 'text/html; charset=utf-8' }
341
+
342
+ console.log "\nOpening browser..."
343
+
344
+ # Open browser
345
+ try
346
+ if process.platform is 'darwin'
347
+ execSync "open 'http://localhost:#{server.port}/'"
348
+ else if process.platform is 'linux'
349
+ execSync "xdg-open 'http://localhost:#{server.port}/'"
350
+ else
351
+ execSync "start 'http://localhost:#{server.port}/'"
352
+ catch
353
+ console.log "Visit: http://localhost:#{server.port}/"
354
+
355
+ # Exit after serving
356
+ setTimeout ->
357
+ if served
358
+ server.stop()
359
+ process.exit(0)
360
+ else
361
+ setTimeout ->
362
+ server.stop()
363
+ process.exit(0)
364
+ , 5000
365
+ , 3000