diff --git a/README.md b/README.md index 24c3c91..04b25ac 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ This project is a reimplementation of the original Python-based [grip](https://g - [x] Todo list like the one on GitHub - Support for github markdown emojis :+1: :bowtie: - Support for mermaid diagrams +- 🌍 **Enhanced multilingual print support** - optimized for global languages including Chinese, Japanese, Korean, Arabic, Hindi, and more +- 🖨️ **Professional print layout** - optimized margins, typography, and page breaks for high-quality printing ```mermaid graph TD; @@ -85,12 +87,41 @@ It's also possible to activate the darkmode: go-grip -d . ``` +To disable automatic browser reload on file changes (useful for stable editing): + +```bash +go-grip --no-reload README.md +``` + To terminate the current server simply press `CTRL-C`. ## :pencil: Examples examples +## :printer: Print Features + +**go-grip** now includes comprehensive print optimization for global users: + +### Multilingual Font Support +- **East Asian**: Chinese (Simplified/Traditional), Japanese, Korean +- **South Asian**: Hindi, Tamil, Bengali, and other Indic languages +- **Middle Eastern**: Arabic, Hebrew with proper RTL support +- **European**: Latin, Cyrillic scripts (Russian, Ukrainian, etc.) +- **Southeast Asian**: Thai, Khmer, and more + +### Print Layout Optimization +- **Smart margins**: Left 2cm (binding edge), others 1.5cm +- **Professional typography**: Optimized font sizes and line spacing +- **Page break control**: Prevents awkward splits in tables, code blocks +- **Clean backgrounds**: Removes backgrounds while preserving colors +- **URL printing**: Shows link destinations for reference + +### Usage +1. Open any markdown file with `go-grip filename.md` +2. Use browser's print function (`Ctrl+P` / `Cmd+P`) +3. Enjoy high-quality multilingual printouts! + ## :bug: Known TODOs / Bugs - [ ] Tests and refactoring diff --git a/cmd/root.go b/cmd/root.go index 899c528..c4adb47 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,6 +17,7 @@ var rootCmd = &cobra.Command{ host, _ := cmd.Flags().GetString("host") port, _ := cmd.Flags().GetInt("port") boundingBox, _ := cmd.Flags().GetBool("bounding-box") + noReload, _ := cmd.Flags().GetBool("no-reload") var file string if len(args) == 1 { @@ -24,7 +25,7 @@ var rootCmd = &cobra.Command{ } parser := pkg.NewParser(theme) - server := pkg.NewServer(host, port, theme, boundingBox, browser, parser) + server := pkg.NewServer(host, port, theme, boundingBox, browser, !noReload, parser) return server.Serve(file) }, } @@ -42,4 +43,5 @@ func init() { rootCmd.Flags().StringP("host", "H", "localhost", "Host to use") rootCmd.Flags().IntP("port", "p", 6419, "Port to use") rootCmd.Flags().Bool("bounding-box", true, "Add bounding box to HTML") + rootCmd.Flags().Bool("no-reload", false, "Disable automatic browser reload on file changes") } diff --git a/defaults/static/css/github-mermaid.css b/defaults/static/css/github-mermaid.css new file mode 100644 index 0000000..0628f52 --- /dev/null +++ b/defaults/static/css/github-mermaid.css @@ -0,0 +1,20 @@ +/* Mermaid error and utility styles */ +.mermaid-error { + background: #fff0f0; + color: #cf222e; + border: 1px solid #cf222e; + border-radius: 6px; + padding: 8px 12px; + margin: 8px 0; + white-space: pre-wrap; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + font-size: 12px; +} + +@media (prefers-color-scheme: dark) { + .mermaid-error { + background: #2a1616; + color: #ff7b72; + border-color: #da3633; + } +} \ No newline at end of file diff --git a/defaults/static/css/github-print.css b/defaults/static/css/github-print.css index 43bc4be..3fa6850 100644 --- a/defaults/static/css/github-print.css +++ b/defaults/static/css/github-print.css @@ -1,9 +1,374 @@ -.container-inner { - border-width: 0; - border-color: transparent; - border-style: none; - border-radius: 0; - margin-top: 0; - margin-bottom: 0; - padding: 0; +@media print { + /* =========================================== + * COMPREHENSIVE PRINT OPTIMIZATION + * Supports global multilingual typography + * =========================================== */ + + /* ========== GLOBAL RESET & LAYOUT ========== */ + * { + /* Universal multilingual font stack optimized for print */ + font-family: + /* System fonts - best quality */ + -apple-system, BlinkMacSystemFont, "Segoe UI", + /* Western European languages */ + "Helvetica Neue", Helvetica, Arial, + /* East Asian languages */ + "Microsoft YaHei", "微软雅黑", /* Simplified Chinese (Windows) */ + "PingFang SC", "苹方-简", /* Simplified Chinese (macOS) */ + "Hiragino Sans GB", "冬青黑体简体中文", /* Simplified Chinese (macOS legacy) */ + "Microsoft JhengHei", "微軟正黑體", /* Traditional Chinese (Windows) */ + "PingFang TC", "苹方-繁", /* Traditional Chinese (macOS) */ + "Hiragino Sans CNS", "冬青黑体简体中文", /* Traditional Chinese (macOS legacy) */ + "Yu Gothic", "游ゴシック", "YuGothic", /* Japanese (Windows 8.1+) */ + "Hiragino Kaku Gothic ProN", "ヒラギノ角ゴ ProN W3", /* Japanese (macOS) */ + "Meiryo", "メイリオ", /* Japanese (Windows legacy) */ + "Malgun Gothic", "맑은 고딕", /* Korean (Windows) */ + "Apple SD Gothic Neo", /* Korean (macOS) */ + /* Indic languages */ + "Noto Sans Devanagari", "Mangal", /* Hindi, Sanskrit, Marathi */ + "Noto Sans Tamil", "Latha", /* Tamil */ + "Noto Sans Bengali", "Vrinda", /* Bengali */ + "Noto Sans Gujarati", "Shruti", /* Gujarati */ + "Noto Sans Telugu", "Gautami", /* Telugu */ + "Noto Sans Kannada", "Tunga", /* Kannada */ + "Noto Sans Malayalam", "Kartika", /* Malayalam */ + "Noto Sans Oriya", "Kalinga", /* Oriya */ + "Noto Sans Gurmukhi", "Raavi", /* Punjabi */ + /* Arabic and Hebrew */ + "Noto Sans Arabic", "Tahoma", "Arial Unicode MS", /* Arabic */ + "Noto Sans Hebrew", "David", "Miriam", /* Hebrew */ + /* Cyrillic */ + "Noto Sans", "DejaVu Sans", "Liberation Sans", /* Russian, Ukrainian, etc. */ + /* Thai and Southeast Asian */ + "Noto Sans Thai", "Tahoma", /* Thai */ + "Noto Sans", "Khmer UI", /* Khmer */ + /* Fallbacks */ + sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji"; + + /* Remove backgrounds but preserve colors for better print readability */ + background: transparent !important; + + /* Remove shadows and effects for cleaner print */ + box-shadow: none !important; + text-shadow: none !important; + } + + /* Page setup with optimized margins */ + @page { + margin-top: 1.5cm; + margin-right: 1.5cm; + margin-bottom: 1.5cm; + margin-left: 2cm; /* Larger left margin for binding */ + size: A4; + } + + html, body { + background: transparent !important; + font-size: 12pt !important; + line-height: 1.4 !important; + } + + /* ========== CONTAINER & LAYOUT OPTIMIZATION ========== */ + .container { + max-width: none !important; + margin: 0 !important; + padding: 0 !important; + } + + .container-inner { + border: none !important; + border-radius: 0 !important; + margin: 0 !important; + padding: 0 !important; + box-shadow: none !important; + } + + /* Remove footer for print */ + .footer { + display: none !important; + } + + /* ========== TYPOGRAPHY OPTIMIZATION ========== */ + .markdown-body { + font-size: 12pt !important; + line-height: 1.4 !important; + background: transparent !important; + } + + /* Headings optimization */ + .markdown-body h1 { + font-size: 18pt !important; + font-weight: bold !important; + margin-top: 0 !important; + margin-bottom: 12pt !important; + page-break-after: avoid !important; + border-bottom: 2pt solid #333 !important; + padding-bottom: 6pt !important; + } + + .markdown-body h2 { + font-size: 16pt !important; + font-weight: bold !important; + margin-top: 16pt !important; + margin-bottom: 8pt !important; + page-break-after: avoid !important; + border-bottom: 1pt solid #666 !important; + padding-bottom: 4pt !important; + } + + .markdown-body h3 { + font-size: 14pt !important; + font-weight: bold !important; + margin-top: 12pt !important; + margin-bottom: 6pt !important; + page-break-after: avoid !important; + } + + .markdown-body h4, + .markdown-body h5, + .markdown-body h6 { + font-size: 12pt !important; + font-weight: bold !important; + margin-top: 12pt !important; + margin-bottom: 6pt !important; + page-break-after: avoid !important; + } + + /* Paragraph spacing */ + .markdown-body p { + margin-top: 0 !important; + margin-bottom: 8pt !important; + orphans: 3 !important; + widows: 3 !important; + } + + /* ========== LISTS OPTIMIZATION ========== */ + .markdown-body ul, + .markdown-body ol { + margin-top: 0 !important; + margin-bottom: 8pt !important; + padding-left: 20pt !important; + } + + .markdown-body li { + margin-bottom: 4pt !important; + page-break-inside: avoid !important; + } + + /* Task lists */ + .markdown-body .task-list-item { + list-style: none !important; + } + + .markdown-body .task-list-item-checkbox { + margin-right: 6pt !important; + } + + /* ========== CODE BLOCKS OPTIMIZATION ========== */ + .markdown-body code, + .markdown-body tt { + font-family: + "Consolas", "Monaco", "Courier New", + "DejaVu Sans Mono", "Liberation Mono", + "Source Code Pro", "Fira Code", + monospace !important; + font-size: 10pt !important; + background: #f5f5f5 !important; + color: #000 !important; + border: 1pt solid #ddd !important; + border-radius: 2pt !important; + padding: 1pt 3pt !important; + } + + .markdown-body pre { + font-family: + "Consolas", "Monaco", "Courier New", + "DejaVu Sans Mono", "Liberation Mono", + "Source Code Pro", "Fira Code", + monospace !important; + font-size: 9pt !important; + line-height: 1.3 !important; + background: #f8f8f8 !important; + color: #000 !important; + border: 1pt solid #ccc !important; + border-radius: 3pt !important; + padding: 8pt !important; + overflow: visible !important; + white-space: pre-wrap !important; + page-break-inside: avoid !important; + } + + .markdown-body pre code { + background: transparent !important; + border: none !important; + padding: 0 !important; + font-size: inherit !important; + } + + /* ========== TABLES OPTIMIZATION ========== */ + .markdown-body table { + border-collapse: collapse !important; + width: 100% !important; + margin-bottom: 12pt !important; + page-break-inside: avoid !important; + } + + .markdown-body th, + .markdown-body td { + border: 1pt solid #666 !important; + padding: 4pt 8pt !important; + text-align: left !important; + vertical-align: top !important; + } + + .markdown-body th { + background: #f0f0f0 !important; + font-weight: bold !important; + } + + .markdown-body tbody tr:nth-child(even) { + background: #f9f9f9 !important; + } + + /* ========== BLOCKQUOTES & ALERTS ========== */ + .markdown-body blockquote { + border-left: 3pt solid #666 !important; + margin: 8pt 0 !important; + padding: 0 0 0 12pt !important; + font-style: italic !important; + background: #f9f9f9 !important; + page-break-inside: avoid !important; + } + + /* Markdown alerts */ + .markdown-body .markdown-alert { + border: 1pt solid #ccc !important; + border-radius: 3pt !important; + padding: 8pt !important; + margin: 8pt 0 !important; + page-break-inside: avoid !important; + } + + .markdown-body .markdown-alert-note { + border-left: 3pt solid #0969da !important; + background: #f0f8ff !important; + } + + .markdown-body .markdown-alert-tip { + border-left: 3pt solid #1a7f37 !important; + background: #f0fff0 !important; + } + + .markdown-body .markdown-alert-important { + border-left: 3pt solid #8250df !important; + background: #faf0ff !important; + } + + .markdown-body .markdown-alert-warning { + border-left: 3pt solid #9a6700 !important; + background: #fffbf0 !important; + } + + .markdown-body .markdown-alert-caution { + border-left: 3pt solid #cf222e !important; + background: #fff0f0 !important; + } + + /* ========== LINKS OPTIMIZATION ========== */ + .markdown-body a { + text-decoration: underline !important; + } + + /* Print URLs after links */ + .markdown-body a[href]:after { + content: " (" attr(href) ")" !important; + font-size: 9pt !important; + color: #666 !important; + word-break: break-all !important; + } + + /* Don't print URLs for relative links or anchors */ + .markdown-body a[href^="#"]:after, + .markdown-body a[href^="/"]:after, + .markdown-body a[href*="javascript:"]:after { + content: "" !important; + } + + /* ========== IMAGES OPTIMIZATION ========== */ + .markdown-body img { + max-width: 100% !important; + height: auto !important; + page-break-inside: avoid !important; + border: 1pt solid #ddd !important; + } + + /* Emoji should stay inline */ + .markdown-body img.emoji { + border: none !important; + display: inline !important; + margin: 0 !important; + } + + /* ========== NAVIGATION & UI ELEMENTS ========== */ + /* Hide elements that don't make sense in print */ + .anchor, + .octicon, + .task-list-item .handle, + [data-footnote-backref] { + display: none !important; + } + + /* ========== PAGE BREAK CONTROL ========== */ + .markdown-body h1, + .markdown-body h2 { + page-break-before: auto !important; + page-break-after: avoid !important; + } + + .markdown-body h3, + .markdown-body h4, + .markdown-body h5, + .markdown-body h6 { + page-break-after: avoid !important; + } + + .markdown-body pre, + .markdown-body blockquote, + .markdown-body table, + .markdown-body .markdown-alert { + page-break-inside: avoid !important; + } + + /* Avoid orphans and widows */ + .markdown-body p, + .markdown-body li { + orphans: 3 !important; + widows: 3 !important; + } + + /* ========== MERMAID DIAGRAMS ========== */ + /* Mermaid diagrams should be handled gracefully */ + .mermaid { + page-break-inside: avoid !important; + border: 1pt solid #ddd !important; + padding: 8pt !important; + margin: 8pt 0 !important; + } + + /* ========== UTILITY CLASSES ========== */ + .print-break-before { + page-break-before: always !important; + } + + .print-break-after { + page-break-after: always !important; + } + + .print-no-break { + page-break-inside: avoid !important; + } + + .print-hide { + display: none !important; + } } diff --git a/defaults/static/js/mermaid-init.js b/defaults/static/js/mermaid-init.js new file mode 100644 index 0000000..787f162 --- /dev/null +++ b/defaults/static/js/mermaid-init.js @@ -0,0 +1,162 @@ +(function(){ + function computeTheme(){ + try { + var bodyTheme = document.body.getAttribute('data-theme') || 'auto'; + if (bodyTheme === 'dark') return 'dark'; + if (bodyTheme === 'light') return 'default'; + // auto + var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + return prefersDark ? 'dark' : 'default'; + } catch(e){ + return 'default'; + } + } + + function escapeHTML(str){ + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\"/g, '"') + .replace(/'/g, '''); + } + + function getCode(node){ + return (node.dataset && node.dataset.code) ? node.dataset.code : (node.textContent || ''); + } + function setCode(node, code){ + if (node.dataset) node.dataset.code = code; + } + + function renderWithAPI(id, code, node){ + return new Promise(function(resolve, reject){ + try { + // Prefer mermaid.render if available (Mermaid v10) + if (mermaid && typeof mermaid.render === 'function') { + var res1 = mermaid.render(id, code); + if (res1 && typeof res1.then === 'function') { + res1.then(function(out){ + try { + if (out && out.svg) { + node.innerHTML = out.svg; + if (typeof out.bindFunctions === 'function') out.bindFunctions(node); + } + resolve(); + } catch(e1){ reject(e1); } + }).catch(reject); + return; + } else if (res1 && typeof res1.svg === 'string') { + node.innerHTML = res1.svg; + if (typeof res1.bindFunctions === 'function') res1.bindFunctions(node); + resolve(); + return; + } + } + + // Fallback to mermaid.mermaidAPI.render (v8/v9) + if (mermaid && mermaid.mermaidAPI && typeof mermaid.mermaidAPI.render === 'function') { + var cbHandled = false; + var res2 = mermaid.mermaidAPI.render(id, code, function(svgCode, bindFns){ + try { + cbHandled = true; + if (svgCode) { + node.innerHTML = svgCode; + if (typeof bindFns === 'function') bindFns(node); + } + resolve(); + } catch(e2){ reject(e2); } + } /* do not pass container to avoid doc-related issues */); + + if (res2 && typeof res2.then === 'function') { + res2.then(function(out){ + try { + if (out && out.svg) { + node.innerHTML = out.svg; + if (typeof out.bindFunctions === 'function') out.bindFunctions(node); + } + resolve(); + } catch(e3){ reject(e3); } + }).catch(reject); + } else if (typeof res2 === 'string') { + node.innerHTML = res2; + resolve(); + } else if (!cbHandled) { + // Neither promise nor string nor callback? Treat as error + reject(new Error('Unexpected return from mermaidAPI.render')); + } + return; + } + + reject(new Error('No supported Mermaid render API')); + } catch(e){ reject(e); } + }); + } + + async function renderAll(){ + var theme = computeTheme(); + if (!window.mermaid) return; + try { + if (!window.__goGripMermaidInitDone) { + mermaid.initialize({ startOnLoad: false, theme: theme, securityLevel: 'loose', logLevel: 'error' }); + window.__goGripMermaidInitDone = true; + } else { + // Update theme dynamically if needed + if (mermaid && mermaid.initialize) { + mermaid.initialize({ startOnLoad: false, theme: theme, securityLevel: 'loose', logLevel: 'error' }); + } + } + } catch(e) { + console.error('Mermaid init error:', e); + } + + var nodes = Array.prototype.slice.call(document.querySelectorAll('.mermaid')); + + // Sequential rendering to avoid race conditions / global state issues + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + try { + var code = getCode(node).trim(); + setCode(node, code); + if (mermaid.mermaidAPI && mermaid.mermaidAPI.parse) { + try { + mermaid.mermaidAPI.parse(code); + } catch (parseErr) { + console.error('Mermaid parse error:', parseErr, { index: i, code: code }); + node.classList.add('mermaid-error'); + node.innerHTML = '
Mermaid parse error:\\n' + escapeHTML(parseErr.str || parseErr.message || String(parseErr)) + '
'; + continue; + } + } + + var id = 'mermaid-svg-' + i + '-' + Date.now(); + try { + await renderWithAPI(id, code, node); + node.classList.remove('mermaid-error'); + } catch(runErr){ + console.error('Mermaid render error:', runErr, { index: i, code: code }); + node.classList.add('mermaid-error'); + node.innerHTML = '
Mermaid render error:\\n' + escapeHTML(runErr && runErr.message || String(runErr)) + '
'; + } + } catch(err){ + console.error('Mermaid error:', err, { index: i }); + node.classList.add('mermaid-error'); + node.innerHTML = '
Mermaid error:\\n' + escapeHTML(err.message || String(err)) + '
'; + } + } + } + + document.addEventListener('DOMContentLoaded', function(){ + renderAll(); + + // Re-render on theme change when in auto mode + var themeAttr = document.body.getAttribute('data-theme') || 'auto'; + if (themeAttr === 'auto' && window.matchMedia) { + var mq = window.matchMedia('(prefers-color-scheme: dark)'); + if (mq && mq.addEventListener) { + mq.addEventListener('change', function(){ + renderAll(); + }); + } + } + }); +})(); \ No newline at end of file diff --git a/defaults/templates/layout.html b/defaults/templates/layout.html index 87f4545..306a35a 100644 --- a/defaults/templates/layout.html +++ b/defaults/templates/layout.html @@ -25,9 +25,10 @@ {{end}} + - +
{{ .Content }} @@ -36,5 +37,7 @@ {{if .BoundingBox}}
Made with ♥ by chrishrb
{{end}} + + diff --git a/defaults/templates/mermaid/mermaid.html b/defaults/templates/mermaid/mermaid.html index 95a8f71..4e1c94c 100644 --- a/defaults/templates/mermaid/mermaid.html +++ b/defaults/templates/mermaid/mermaid.html @@ -2,23 +2,4 @@
{{ .Content }}
- - - {{if eq .Theme "dark" }} - - {{else if eq .Theme "light" }} - - {{else}} - - {{end}}
diff --git a/pkg/server.go b/pkg/server.go index bd331d8..f0fa4cc 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -25,16 +25,18 @@ type Server struct { host string port int browser bool + enableReload bool } -func NewServer(host string, port int, theme string, boundingBox bool, browser bool, parser *Parser) *Server { +func NewServer(host string, port int, theme string, boundingBox bool, browser bool, enableReload bool, parser *Parser) *Server { return &Server{ - host: host, - port: port, - theme: theme, - boundingBox: boundingBox, - browser: browser, - parser: parser, + host: host, + port: port, + theme: theme, + boundingBox: boundingBox, + browser: browser, + enableReload: enableReload, + parser: parser, } } @@ -42,8 +44,15 @@ func (s *Server) Serve(file string) error { directory := path.Dir(file) filename := path.Base(file) - reload := reload.New(directory) - reload.DebugLog = log.New(io.Discard, "", 0) + var reloadMiddleware *reload.Reloader + if s.enableReload { + reloadMiddleware = reload.New(directory) + reloadMiddleware.DebugLog = log.New(io.Discard, "", 0) + // Fix WebSocket CORS issues for development + reloadMiddleware.Upgrader.CheckOrigin = func(r *http.Request) bool { + return true + } + } validThemes := map[string]bool{"light": true, "dark": true, "auto": true} @@ -117,7 +126,13 @@ func (s *Server) Serve(file string) error { } } - handler := reload.Handle(http.DefaultServeMux) + var handler http.Handler = http.DefaultServeMux + if s.enableReload { + handler = reloadMiddleware.Handle(http.DefaultServeMux) + fmt.Printf("📡 Auto-reload enabled. Files will trigger browser refresh.\n") + } else { + fmt.Printf("🔄 Auto-reload disabled. Use F5 to manually refresh.\n") + } return http.ListenAndServe(fmt.Sprintf(":%d", s.port), handler) }