ohhighmark 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f310c11edeb0bc2c0b7ce0871e3fce1bac23bc79d8bc06b6ae2a5612628afc9e
4
+ data.tar.gz: 1221b0fc060bc43e0a5c771e3dc51008ff41a6b883b977c926d59e34b3c49f21
5
+ SHA512:
6
+ metadata.gz: 6da2f05b511406d14d93c64052b392220cf3ccb31cffef58b28a35daf940cf7275e09dbfe1a3dfcf863f92bba6f37a75d58d79d4726e3d9d700c7f3cb45a002b
7
+ data.tar.gz: a0a97b44f53f5f52ee45eeea9ae8bb90a9cfc453c6a4d0ef60740a7380a04313fb1390e944ae0c1fa38f02f76e9c9c53991f945853c661870147eb8d2d05b72e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 David Knight
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.
data/README.md ADDED
@@ -0,0 +1,346 @@
1
+ # OhHighMark :wave:
2
+
3
+ **A Ruby library for syntax highlighting markdown code with beautiful, customizable output.**
4
+
5
+ OhHighMark provides colorful and readable markdown syntax visualization by tokenizing and rendering markdown with appropriate CSS classes. It displays each line in a table row with line numbers, ensuring perfect alignment and preserving all whitespace.
6
+
7
+ ## Overview
8
+
9
+ ### Key Features
10
+
11
+ - **Table-based layout** - Each line in its own table row with separate cells for line numbers and code
12
+ - **Perfect alignment** - Line numbers stay aligned with code content, even with long lines
13
+ - **Whitespace preservation** - Uses `white-space: pre` to maintain exact spacing and indentation
14
+ - **Non-selectable line numbers** - Prevents copy/paste issues
15
+ - **Comprehensive markdown support** - Headings, text formatting, code, links, lists, tables, quotes, and more
16
+ - **Inline markdown in lists** - Bold, italic, links, and other inline syntax work within list items
17
+ - **Customizable styling** - Pre-built CSS with CSS variables for easy theming
18
+ - **Semantic HTML** - Clean, semantic markup with `data-line-number` on rows
19
+ - **Flexible rules** - Customizable tokenization rules via DSL
20
+
21
+ ### Supported Markdown Syntax
22
+
23
+ OhHighMark supports all standard markdown elements:
24
+
25
+ **Text Formatting:**
26
+ - **Bold**: `**text**`
27
+ - _Italic_: `_text_`
28
+ - ***Bold-Italic***: `***text***`
29
+ - ~~Strikethrough~~: `~~text~~`
30
+
31
+ **Headings:** `# H1` through `###### H6`
32
+
33
+ **Code:**
34
+ - Inline: `` `code` ``
35
+ - Fenced blocks: ` ``` ` ... ` ``` `
36
+
37
+ **Links:**
38
+ - Standard: `[text](url)`
39
+ - Auto-links: `<http://url>`
40
+
41
+ **Lists:**
42
+ - Unordered: `- item`, `* item`, `+ item`
43
+ - Nested: ` - nested item`
44
+ - **With inline markdown**: `- **bold** with [link](url)`
45
+
46
+ **Tables:**
47
+ ```markdown
48
+ | Column 1 | Column 2 |
49
+ | -------- | -------- |
50
+ | Value 1 | Value 2 |
51
+ ```
52
+
53
+ **Other:**
54
+ - Horizontal rules: `---`
55
+ - Block quotes: `> quote`
56
+ - Escaped characters: `\*`, `\[`, `\]`, `\\`
57
+
58
+ ## Installation
59
+
60
+ Add to your Gemfile:
61
+
62
+ ```ruby
63
+ gem 'ohhighmark'
64
+ ```
65
+
66
+ Then run:
67
+
68
+ ```bash
69
+ $ bundle install
70
+ ```
71
+
72
+ Or install directly:
73
+
74
+ ```bash
75
+ $ gem install ohhighmark
76
+ ```
77
+
78
+ ## Usage
79
+
80
+ ### Basic Usage
81
+
82
+ ```ruby
83
+ require 'ohhighmark'
84
+
85
+ # Use the helper in your views (Rails, Middleman, etc.)
86
+ include OhHighMark::Helper
87
+
88
+ # In your ERB view:
89
+ <%= highlight_markdown do %>
90
+ # Your Markdown Here
91
+
92
+ This is **bold** and this is _italic_.
93
+
94
+ - List item with [link](https://example.com)
95
+ - Item with `inline code`
96
+ <% end %>
97
+ ```
98
+
99
+ ### Direct API Usage
100
+
101
+ ```ruby
102
+ require 'ohhighmark'
103
+
104
+ markdown = <<~MD
105
+ # Welcome
106
+
107
+ This is **bold** and ~~strikethrough~~.
108
+ MD
109
+
110
+ # Create highlighter and process
111
+ highlighter = OhHighMark::Highlighter.new
112
+ html_output = highlighter.process_lines_with_linenums(markdown)
113
+ ```
114
+
115
+ ### Advanced: Custom Rules
116
+
117
+ ```ruby
118
+ # Define custom markdown rules
119
+ ruleset = OhHighMark::RuleSet.new
120
+ OhHighMark::MarkdownRules.apply(ruleset)
121
+
122
+ # Add custom rules if needed
123
+ ruleset.add(:custom_pattern, /your_regex/, :inline)
124
+
125
+ # Create tokenizer and renderer
126
+ tokenizer = OhHighMark::Tokenizer.new(ruleset.rules)
127
+ tokens = tokenizer.tokenize(markdown_text)
128
+
129
+ renderer = OhHighMark::Renderer.new
130
+ html = renderer.render(tokens)
131
+ ```
132
+
133
+ ## Styling
134
+
135
+ OhHighMark includes a pre-built CSS stylesheet with CSS custom properties (CSS variables) for easy customization.
136
+
137
+ ### Including the Stylesheet
138
+
139
+ **Rails (Asset Pipeline / Sprockets):**
140
+
141
+ In `application.css`:
142
+ ```css
143
+ /*
144
+ *= require ohhighmark/ohhighmark
145
+ */
146
+ ```
147
+
148
+ Or in `application.scss`:
149
+ ```scss
150
+ @import 'ohhighmark/ohhighmark';
151
+ ```
152
+
153
+ **Rails (Propshaft / Importmap):**
154
+
155
+ Copy the stylesheet:
156
+ ```bash
157
+ $ cp $(bundle show ohhighmark)/app/assets/stylesheets/ohhighmark/ohhighmark.css app/assets/stylesheets/
158
+ ```
159
+
160
+ Link in your layout:
161
+ ```erb
162
+ <%= stylesheet_link_tag "ohhighmark" %>
163
+ ```
164
+
165
+ **Middleman:**
166
+
167
+ Copy the stylesheet:
168
+ ```bash
169
+ $ cp $(bundle show ohhighmark)/app/assets/stylesheets/ohhighmark/ohhighmark.css source/stylesheets/
170
+ ```
171
+
172
+ Then import it in your main stylesheet.
173
+
174
+ **Standalone / Other Frameworks:**
175
+
176
+ Find the stylesheet in the gem:
177
+ ```bash
178
+ $ bundle show ohhighmark
179
+ # Copy app/assets/stylesheets/ohhighmark/ohhighmark.css to your project
180
+ ```
181
+
182
+ ### Customizing Colors
183
+
184
+ Override CSS variables **after** including the OhHighMark stylesheet:
185
+
186
+ ```css
187
+ :root {
188
+ /* Text and background */
189
+ --ohm-text-color: #ffffff;
190
+ --ohm-background-color: #1a1a1a;
191
+ --ohm-linenos-color: #666666;
192
+ --ohm-linenos-bg-color: #1a1a1a;
193
+
194
+ /* Syntax colors */
195
+ --ohm-yellow: #ffff00; /* H1 headings */
196
+ --ohm-orange: #ff8000; /* H2 headings */
197
+ --ohm-purple: #ff00ff; /* H3 headings */
198
+ --ohm-cyan: #00ffff; /* H4-H6 headings */
199
+ --ohm-blue: #0080ff; /* Links */
200
+ --ohm-intense-blue: #0040ff; /* Link hover */
201
+ --ohm-green: #00ff00; /* Quotes, lists */
202
+ --ohm-gray: #808080; /* Horizontal rules */
203
+ --ohm-grayish: #999999; /* Table borders */
204
+
205
+ /* Table highlighting */
206
+ --ohm-table-bg: rgba(255, 248, 197, 0.2);
207
+
208
+ /* Font */
209
+ --ohm-monospace-font: "Courier New", monospace;
210
+ }
211
+ ```
212
+
213
+ **Dark Theme Example:**
214
+ ```css
215
+ :root {
216
+ --ohm-text-color: #e0e0e0;
217
+ --ohm-background-color: #0d1117;
218
+ --ohm-linenos-bg-color: #0d1117;
219
+ --ohm-linenos-color: #6e7681;
220
+ --ohm-blue: #4a9eff;
221
+ --ohm-green: #7cfc00;
222
+ --ohm-yellow: #ffd700;
223
+ --ohm-orange: #ff6b35;
224
+ }
225
+ ```
226
+
227
+ **Light Theme Example:**
228
+ ```css
229
+ :root {
230
+ --ohm-text-color: #333333;
231
+ --ohm-background-color: #ffffff;
232
+ --ohm-linenos-bg-color: #f6f8fa;
233
+ --ohm-linenos-color: #57606a;
234
+ --ohm-blue: #0969da;
235
+ --ohm-green: #1a7f37;
236
+ --ohm-yellow: #b8860b;
237
+ --ohm-orange: #d2691e;
238
+ }
239
+ ```
240
+
241
+ ### HTML Structure
242
+
243
+ OhHighMark generates semantic HTML with clean structure:
244
+
245
+ ```html
246
+ <div class="ohhighmark">
247
+ <table class="highlight">
248
+ <tbody>
249
+ <!-- Regular content row -->
250
+ <tr data-line-number="1">
251
+ <td class="blob-num">1</td>
252
+ <td class="blob-code"><span class="md-h1"># Heading</span></td>
253
+ </tr>
254
+
255
+ <!-- Row with text -->
256
+ <tr data-line-number="2">
257
+ <td class="blob-num">2</td>
258
+ <td class="blob-code">Some text with <span class="md-bold">**bold**</span></td>
259
+ </tr>
260
+
261
+ <!-- Table row (has table-line class) -->
262
+ <tr class="table-line" data-line-number="3">
263
+ <td class="blob-num">3</td>
264
+ <td class="blob-code"><span class="md-table-pipe">|</span> Col 1 <span class="md-table-pipe">|</span> Col 2 <span class="md-table-pipe">|</span></td>
265
+ </tr>
266
+ </tbody>
267
+ </table>
268
+ </div>
269
+ ```
270
+
271
+ **Key Structure Points:**
272
+ - `data-line-number` attribute on `<tr>` for easy row targeting
273
+ - `.blob-num` cell contains line number
274
+ - `.blob-code` cell contains formatted content
275
+ - `.table-line` class added to rows that are part of markdown tables
276
+ - Markdown syntax wrapped in `<span>` elements with `.md-*` classes
277
+
278
+ ### CSS Classes Reference
279
+
280
+ **Container & Layout:**
281
+ - `.ohhighmark` - Main container div
282
+ - `table.highlight` - Table element containing all lines
283
+ - `.blob-num` - Line number cell (`user-select: none` prevents copying)
284
+ - `.blob-code` - Code content cell (uses `white-space: pre`)
285
+ - `.table-line` - Added to `<tr>` for markdown table rows
286
+
287
+ **Markdown Syntax Classes:**
288
+ - `.md-h1` through `.md-h6` - Heading levels 1-6
289
+ - `.md-bold` - Bold text (`**text**`)
290
+ - `.md-italic` - Italic text (`_text_`)
291
+ - `.md-em` - Bold-italic emphasis (`***text***`)
292
+ - `.md-strikethrough` - Strikethrough (`~~text~~`)
293
+ - `.md-code` - Inline code (`` `code` ``)
294
+ - `.md-link` - Standard links (`[text](url)`)
295
+ - `.md-autolink` - Auto-linked URLs (`<http://url>`)
296
+ - `.md-li` - List item markers
297
+ - `.md-hr` - Horizontal rules (`---`)
298
+ - `.md-fence` - Fenced code block delimiters
299
+ - `.md-codeblock` - Content within fenced code blocks
300
+ - `.md-table-pipe` - Table pipe characters (`|`)
301
+ - `.md-table-dash` - Table separator rows (`---`)
302
+ - `.md-quote` - Block quotes (`> text`)
303
+
304
+ ### Critical CSS Requirements
305
+
306
+ **The `.blob-code` cell MUST have `white-space: pre`** to preserve whitespace and prevent wrapping:
307
+
308
+ ```css
309
+ .ohhighmark td.blob-code {
310
+ white-space: pre; /* REQUIRED - preserves all whitespace */
311
+ font-family: ui-monospace, monospace;
312
+ line-height: 20px;
313
+ }
314
+ ```
315
+
316
+ ### Frontend JavaScript Integration
317
+
318
+ Target rows by line number:
319
+
320
+ ```javascript
321
+ // Select a specific line
322
+ const row = document.querySelector('[data-line-number="5"]');
323
+
324
+ // Highlight a line
325
+ row.style.backgroundColor = 'rgba(255, 255, 0, 0.2)';
326
+
327
+ // Get all table rows
328
+ const tableRows = document.querySelectorAll('tr[data-line-number]');
329
+ ```
330
+
331
+ ## Contributing
332
+
333
+ Bug reports and pull requests are welcome!
334
+
335
+ ### Development Setup
336
+
337
+ ```bash
338
+ $ git clone https://github.com/viacoffee/ohhighmark.git
339
+ $ cd ohhighmark
340
+ $ bundle install
341
+ $ rspec # Run tests
342
+ ```
343
+
344
+ ## License
345
+
346
+ This gem is available as open source under the terms of the [MIT License](LICENSE).
@@ -0,0 +1,171 @@
1
+ /**
2
+ * OhHighMark Stylesheet
3
+ *
4
+ * CSS custom properties for easy customization.
5
+ * Override these variables to customize the color scheme.
6
+ */
7
+
8
+ :root {
9
+ /* Colors used by OhHighMark */
10
+ --ohm-text-color: #d8dee9; /* Main text color */
11
+ --ohm-background-color: #2B303B; /* Background color */
12
+ --ohm-linenos-color: #3b414d; /* Line numbers color */
13
+ --ohm-linenos-bg-color: #2B303B; /* Line numbers background */
14
+ --ohm-gray: #4c566a; /* Gray for HR */
15
+ --ohm-green: #a3be8c; /* Green for quotes and lists */
16
+ --ohm-grayish: #667084; /* Light gray for tables */
17
+ --ohm-blue: #81a1c1; /* Blue for links */
18
+ --ohm-intense-blue: #5e81ac; /* Darker blue for link hover */
19
+ --ohm-yellow: #ebcb8b; /* Yellow for h1 */
20
+ --ohm-orange: #d08770; /* Orange for h2 */
21
+ --ohm-purple: #b48ead; /* Purple for h3 */
22
+ --ohm-cyan: #8fbcbb; /* Cyan for h4-h6 */
23
+ --ohm-table-bg: rgba(255, 248, 197, 0.2); /* Table row highlight */
24
+
25
+ /* Font family for monospace content */
26
+ --ohm-monospace-font: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
27
+ }
28
+
29
+ .ohhighmark {
30
+ color: var(--ohm-text-color);
31
+ background-color: var(--ohm-background-color);
32
+ padding: 10px;
33
+ margin: 0;
34
+ }
35
+
36
+ /* Table structure */
37
+ .ohhighmark table.highlight {
38
+ border-collapse: collapse;
39
+ width: 100%;
40
+ font-family: var(--ohm-monospace-font);
41
+ font-size: 12px;
42
+ line-height: 20px;
43
+ }
44
+
45
+ .ohhighmark table.highlight tr {
46
+ background: transparent;
47
+ }
48
+
49
+ /* Line number cell */
50
+ .ohhighmark td.blob-num {
51
+ padding: 0 10px;
52
+ width: 1%;
53
+ min-width: 30px;
54
+ font-family: var(--ohm-monospace-font);
55
+ font-size: 12px;
56
+ line-height: 18px;
57
+ color: var(--ohm-linenos-color);
58
+ text-align: right;
59
+ white-space: nowrap;
60
+ vertical-align: top;
61
+ user-select: none;
62
+ background-color: var(--ohm-linenos-bg-color);
63
+ }
64
+
65
+ /* Code content cell - CRITICAL: white-space: pre preserves spaces and line breaks */
66
+ .ohhighmark td.blob-code {
67
+ padding: 0 10px;
68
+ color: var(--ohm-text-color);
69
+ vertical-align: top;
70
+ overflow: visible;
71
+ white-space: pre-wrap; /* IMPORTANT: Preserves whitespace */
72
+ font-family: var(--ohm-monospace-font);
73
+ font-size: 12px;
74
+ line-height: 18px;
75
+ }
76
+
77
+ /* Table row highlighting */
78
+ .ohhighmark tr.table-line {
79
+ background: var(--ohm-table-bg);
80
+ }
81
+
82
+ /* Headings */
83
+ .ohhighmark .md-h1 {
84
+ color: var(--ohm-yellow);
85
+ font-weight: bold;
86
+ }
87
+
88
+ .ohhighmark .md-h2 {
89
+ color: var(--ohm-orange);
90
+ font-weight: bold;
91
+ }
92
+
93
+ .ohhighmark .md-h3 {
94
+ color: var(--ohm-purple);
95
+ font-weight: bold;
96
+ }
97
+
98
+ .ohhighmark .md-h4,
99
+ .ohhighmark .md-h5,
100
+ .ohhighmark .md-h6 {
101
+ color: var(--ohm-cyan);
102
+ font-weight: bold;
103
+ }
104
+
105
+ /* Text styles */
106
+ .ohhighmark .md-bold {
107
+ font-weight: 700;
108
+ }
109
+
110
+ .ohhighmark .md-italic {
111
+ font-style: italic;
112
+ }
113
+
114
+ .ohhighmark .md-em {
115
+ font-weight: 700;
116
+ font-style: italic;
117
+ }
118
+
119
+ .ohhighmark .md-strikethrough {
120
+ text-decoration: line-through;
121
+ }
122
+
123
+ .ohhighmark .md-quote {
124
+ font-style: italic;
125
+ color: var(--ohm-green);
126
+ }
127
+
128
+ /* Horizontal rule */
129
+ .ohhighmark .md-hr {
130
+ color: var(--ohm-gray);
131
+ }
132
+
133
+ /* Lists */
134
+ .ohhighmark .md-li {
135
+ color: var(--ohm-green);
136
+ }
137
+
138
+ /* Links */
139
+ .ohhighmark .md-link,
140
+ .ohhighmark .md-autolink {
141
+ color: var(--ohm-blue);
142
+ text-decoration: underline;
143
+ text-underline-offset: 2px;
144
+ }
145
+
146
+ .ohhighmark .md-link:link,
147
+ .ohhighmark .md-link:visited,
148
+ .ohhighmark .md-autolink:link,
149
+ .ohhighmark .md-autolink:visited {
150
+ color: var(--ohm-blue);
151
+ }
152
+
153
+ .ohhighmark .md-link:hover,
154
+ .ohhighmark .md-link:focus,
155
+ .ohhighmark .md-autolink:hover,
156
+ .ohhighmark .md-autolink:focus {
157
+ color: var(--ohm-intense-blue);
158
+ }
159
+
160
+ /* Tables */
161
+ .ohhighmark .md-table-pipe,
162
+ .ohhighmark .md-table-dash {
163
+ color: var(--ohm-grayish);
164
+ }
165
+
166
+ /* Code blocks and fences */
167
+ .ohhighmark .md-fence,
168
+ .ohhighmark .md-codeblock,
169
+ .ohhighmark .md-code {
170
+ font-family: var(--ohm-monospace-font);
171
+ }
@@ -0,0 +1,14 @@
1
+ module OhHighMark
2
+ module DSL
3
+ private
4
+
5
+ # DSL methods for defining rules
6
+ def define_inline(name, regex, **options)
7
+ add(name, regex, kind: :inline, **options)
8
+ end
9
+
10
+ def define_block(name, regex, **options)
11
+ add(name, regex, kind: :block, **options)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ module OhHighMark
2
+ module Helper
3
+ def highlight_markdown(&block)
4
+ raise ArgumentError, "block required" unless block
5
+
6
+ content =
7
+ if respond_to?(:capture)
8
+ capture(&block)
9
+ else
10
+ block.call.to_s
11
+ end
12
+
13
+ highlighter = Highlighter.new
14
+ html = highlighter.process_lines_with_linenums(content)
15
+
16
+ if respond_to?(:concat)
17
+ concat(html)
18
+ else
19
+ respond_to?(:raw) ? raw(html) : html
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,76 @@
1
+ module OhHighMark
2
+ class Highlighter
3
+ def initialize
4
+ @ruleset = RuleSet.new
5
+ MarkdownRules.apply(@ruleset)
6
+ @tokenizer = Tokenizer.new(@ruleset.rules)
7
+ @renderer = Renderer.new
8
+ @in_fence = false
9
+ end
10
+
11
+ # Output table with one row per line
12
+ def process_lines_with_linenums(content)
13
+ lines = content.each_line.to_a
14
+ current_line_num = 1
15
+ rows = []
16
+
17
+ lines.each do |line|
18
+ # Process the line
19
+ processed_html = process_line(line)
20
+
21
+ # Check if this is a table line
22
+ is_table_line = line_is_table?(line)
23
+
24
+ # create table row
25
+ rows << create_table_row(current_line_num, processed_html, is_table_line)
26
+ current_line_num += 1
27
+ end
28
+
29
+ %(<div class="ohhighmark"><table class="highlight"><tbody>#{rows.join}</tbody></table></div>)
30
+ end
31
+
32
+ def process_line(line)
33
+ if fence_rule&.regex&.match?(line)
34
+ @in_fence = !@in_fence
35
+ return wrap("md-fence", line)
36
+ end
37
+
38
+ return wrap("md-codeblock", line) if @in_fence
39
+
40
+ tokens = @tokenizer.tokenize(line) # block and inline tokens
41
+ @renderer.render(tokens)
42
+ end
43
+
44
+ private
45
+
46
+ def fence_rule
47
+ @ruleset.block.find { |r| r.name == :fence }
48
+ end
49
+
50
+ def wrap(klass, text)
51
+ %(<span class="#{klass}">#{CGI.escapeHTML(text)}</span>)
52
+ end
53
+
54
+ # Checks if a line is part of a markdown table
55
+ def line_is_table?(line)
56
+ # Check against table rules
57
+ table_dash_rule = @ruleset.inline.find { |r| r.name == :table_dash }
58
+ table_syntax_rule = @ruleset.inline.find { |r| r.name == :table_syntax }
59
+
60
+ (table_dash_rule&.regex&.match?(line) || table_syntax_rule&.regex&.match?(line))
61
+ end
62
+
63
+ # Creates a single table row with line number and code cells
64
+ def create_table_row(line_num, code_html, is_table_line = false)
65
+ line_num_display = line_num.to_s
66
+
67
+ # Only include data-line-number attribute if there's an actual line number
68
+ data_attr = line_num_display.empty? ? '' : %( data-line-number="#{line_num}")
69
+
70
+ # Add table-line class if this is a table row
71
+ tr_class = is_table_line ? ' class="table-line"' : ''
72
+
73
+ %(<tr#{tr_class}#{data_attr}><td class="blob-num">#{line_num_display}</td><td class="blob-code">#{code_html}</td></tr>)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,39 @@
1
+ module OhHighMark
2
+ module MarkdownRules
3
+ def self.apply(ruleset)
4
+ ruleset.extend(DSL)
5
+
6
+ ruleset.instance_eval do
7
+ # Block rules
8
+ define_block :fence, /^```/
9
+ # Heading rules for levels 1-6
10
+ (1..6).each do |level|
11
+ define_block "h#{level}".to_sym,
12
+ /^#{'#' * level}\s+.+$/ ,
13
+ class: "md-h#{level}"
14
+ end
15
+ # HR
16
+ define_block :hr, /^---$/
17
+ # Lists
18
+ define_block :list_item, /^(\s*)([-*+])(\s+.+)$/
19
+
20
+ # Inline rules - order matters! More specific patterns first
21
+ define_inline :code, /`([^`]+)`/
22
+ # Emphasis (must be checked before bold to handle ***text***)
23
+ define_inline :em, /\*\*\*(.+?)\*\*\*/
24
+ define_inline :bold, /\*\*(.+?)\*\*/
25
+ define_inline :italic, /\_(.+?)\_/
26
+ define_inline :strikethrough, /~~(.+?)~~/
27
+ define_inline :autolink, /<(https?:\/\/[^\s>]+)>/
28
+ # Escaped characters - before other patterns that might match them
29
+ define_inline :escape, /\\([\*\[\]\\])/
30
+ define_inline :quote, /\"(.+?)\"/
31
+ define_inline :link, /\[(.+?)\]\((.+?)\)/
32
+ # Table separator line (eg: | ---- | ------ |)
33
+ define_inline :table_dash, /^\|(?:\s*-+\s*\|)+\s*$/
34
+ # Table Column/header content: | something |
35
+ define_inline :table_syntax, /^\|.*?\|.*$/
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,79 @@
1
+ require "cgi"
2
+
3
+ module OhHighMark
4
+ class Renderer
5
+ def render(tokens)
6
+ tokens.map { |t| render_token(t) }.join
7
+ end
8
+
9
+ private
10
+
11
+ def render_token(token)
12
+ case token.type
13
+ when :text
14
+ escape(token.raw)
15
+ when :escape
16
+ # For escaped characters, just render the character without the backslash
17
+ token.raw[1..-1]
18
+ when :autolink
19
+ wrap_autolink(token)
20
+ when :link, :list_item, :table_syntax, :table_dash
21
+ send("wrap_#{token.type}", token)
22
+ else
23
+ wrap("md-#{token.type}", token.raw)
24
+ end
25
+ end
26
+
27
+ def escape(text)
28
+ CGI.escapeHTML(text)
29
+ end
30
+
31
+ def wrap(klass, text)
32
+ %(<span class="#{klass}">#{escape(text)}</span>)
33
+ end
34
+
35
+ def wrap_table_syntax(token)
36
+ token.raw.gsub(/\|/) do |pipe|
37
+ wrap("md-table-pipe", pipe)
38
+ end
39
+ end
40
+
41
+ def wrap_table_dash(token)
42
+ wrap("md-table-dash", token.raw)
43
+ end
44
+
45
+ def wrap_list_item(token)
46
+ indent, flag = [
47
+ token.meta[:indent],
48
+ token.meta[:flag]
49
+ ].map(&method(:escape))
50
+
51
+ # Render inline tokens in the rest content
52
+ rest_html = if token.meta[:rest_tokens] && !token.meta[:rest_tokens].empty?
53
+ token.meta[:rest_tokens].map { |t| render_token(t) }.join
54
+ else
55
+ escape(token.meta[:rest])
56
+ end
57
+
58
+ %(#{indent}<span class="md-li">#{flag}</span>#{rest_html})
59
+ end
60
+
61
+ def wrap_link(token)
62
+ raw, url = [
63
+ token.raw,
64
+ token.url
65
+ ].map(&method(:escape))
66
+
67
+ %(<a href="#{url}" target="_blank" class="md-link">#{raw}</a>)
68
+ end
69
+
70
+ def wrap_autolink(token)
71
+ raw, url = [
72
+ token.raw,
73
+ token.url
74
+ ].map(&method(:escape))
75
+
76
+ %(<a href="#{url}" target="_blank" class="md-autolink">#{raw}</a>)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,3 @@
1
+ module OhHighMark
2
+ Rule = Struct.new(:name, :regex, :kind, :options, keyword_init: true)
3
+ end
@@ -0,0 +1,23 @@
1
+ module OhHighMark
2
+ class RuleSet
3
+ attr_reader :rules
4
+
5
+ def initialize
6
+ @rules = []
7
+ end
8
+
9
+ # Public readers for consumers
10
+ def inline
11
+ @rules.select { |r| r.kind == :inline }
12
+ end
13
+
14
+ def block
15
+ @rules.select { |r| r.kind == :block }
16
+ end
17
+
18
+ # Internal method used by DSL
19
+ def add(name, regex, kind:, **options)
20
+ @rules << Rule.new(name: name, regex: regex, kind: kind, options: options)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module OhHighMark
2
+ Token = Struct.new(:type, :raw, :meta, keyword_init: true) do
3
+ # Convenience accessors
4
+ def url
5
+ meta && meta[:url]
6
+ end
7
+
8
+ def flag
9
+ meta && meta[:flag]
10
+ end
11
+
12
+ def rest
13
+ meta && meta[:rest]
14
+ end
15
+
16
+ def indent
17
+ meta && meta[:indent]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,129 @@
1
+ module OhHighMark
2
+ class Tokenizer
3
+ def initialize(rules)
4
+ @block_rules = rules.select { |r| r.kind == :block }
5
+ @inline_rules = rules.select { |r| r.kind == :inline }
6
+ end
7
+
8
+ def tokenize_line(line)
9
+ tokens = []
10
+ rest = line.dup
11
+
12
+ #block rules first
13
+ @block_rules.each do |rule|
14
+ if (m = rule.regex.match(line))
15
+ tokens << build_token(rule, m)
16
+ return tokens # whole line consumed by block
17
+ end
18
+ end
19
+
20
+ # process inline rules
21
+ rest = line.dup
22
+ until rest.empty?
23
+ matched = false
24
+
25
+ @inline_rules.each do |rule|
26
+ if (m = rule.regex.match(rest))
27
+ # emit text before match
28
+ if m.begin(0) > 0
29
+ tokens << Token.new(type: :text, raw: rest[0...m.begin(0)])
30
+ end
31
+
32
+ # emit matched token
33
+ tokens << build_token(rule, m)
34
+
35
+ # slice matched portion
36
+ rest = rest[m.end(0)..] || ""
37
+ matched = true
38
+ break
39
+ end
40
+ end
41
+
42
+ unless matched
43
+ tokens << Token.new(type: :text, raw: rest)
44
+ break
45
+ end
46
+ end
47
+
48
+ tokens
49
+ end
50
+
51
+ def tokenize(content)
52
+ content.each_line.flat_map { |line| tokenize_line(line.chomp) }
53
+ end
54
+
55
+ private
56
+
57
+ def build_token(rule, match)
58
+ case rule.name
59
+ when :link
60
+ Token.new(
61
+ type: :link,
62
+ raw: match[0], # "[text](url)"
63
+ meta: { url: match[2] }
64
+ )
65
+ when :autolink
66
+ Token.new(
67
+ type: :autolink,
68
+ raw: match[0], # "<http://url>"
69
+ meta: { url: match[1] }
70
+ )
71
+ when :escape
72
+ Token.new(
73
+ type: :escape,
74
+ raw: match[0]
75
+ )
76
+ when :list_item
77
+ Token.new(
78
+ type: rule.name,
79
+ raw: match[0],
80
+ meta: {
81
+ indent: match[1],
82
+ flag: match[2],
83
+ rest: match[3],
84
+ rest_tokens: tokenize_inline(match[3]) # Process inline tokens in rest
85
+ }
86
+ )
87
+ else
88
+ Token.new(
89
+ type: rule.name,
90
+ raw: match[0]
91
+ )
92
+ end
93
+ end
94
+
95
+ # Process inline rules on a text fragment
96
+ def tokenize_inline(text)
97
+ tokens = []
98
+ rest = text.dup
99
+
100
+ until rest.empty?
101
+ matched = false
102
+
103
+ @inline_rules.each do |rule|
104
+ if (m = rule.regex.match(rest))
105
+ # emit text before match
106
+ if m.begin(0) > 0
107
+ tokens << Token.new(type: :text, raw: rest[0...m.begin(0)])
108
+ end
109
+
110
+ # emit matched token
111
+ tokens << build_token(rule, m)
112
+
113
+ # slice matched portion
114
+ rest = rest[m.end(0)..] || ""
115
+ matched = true
116
+ break
117
+ end
118
+ end
119
+
120
+ unless matched
121
+ tokens << Token.new(type: :text, raw: rest)
122
+ break
123
+ end
124
+ end
125
+
126
+ tokens
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,3 @@
1
+ module OhHighMark
2
+ VERSION = "0.1.0"
3
+ end
data/lib/ohhighmark.rb ADDED
@@ -0,0 +1,13 @@
1
+ require_relative "ohhighmark/version"
2
+ require_relative "ohhighmark/rule"
3
+ require_relative "ohhighmark/ruleset"
4
+ require_relative "ohhighmark/dsl"
5
+ require_relative "ohhighmark/token"
6
+ require_relative "ohhighmark/tokenizer"
7
+ require_relative "ohhighmark/renderer"
8
+ require_relative "ohhighmark/markdown_rules"
9
+ require_relative "ohhighmark/highlighter"
10
+ require_relative "ohhighmark/helper"
11
+
12
+ module OhHighMark
13
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ohhighmark
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - David Knight
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rspec
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.12'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.12'
26
+ description: OhHighMark is a Ruby library for syntax highlighting of markdown code,
27
+ providing colorful and readable markdown syntax visualization.
28
+ email:
29
+ - david+github@knight.sh
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - app/assets/stylesheets/ohhighmark/ohhighmark.css
37
+ - lib/ohhighmark.rb
38
+ - lib/ohhighmark/dsl.rb
39
+ - lib/ohhighmark/helper.rb
40
+ - lib/ohhighmark/highlighter.rb
41
+ - lib/ohhighmark/markdown_rules.rb
42
+ - lib/ohhighmark/renderer.rb
43
+ - lib/ohhighmark/rule.rb
44
+ - lib/ohhighmark/ruleset.rb
45
+ - lib/ohhighmark/token.rb
46
+ - lib/ohhighmark/tokenizer.rb
47
+ - lib/ohhighmark/version.rb
48
+ homepage: https://github.com/viacoffee/ohhighmark
49
+ licenses:
50
+ - MIT
51
+ metadata:
52
+ homepage_uri: https://github.com/viacoffee/ohhighmark
53
+ source_code_uri: https://github.com/viacoffee/ohhighmark
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 3.6.9
69
+ specification_version: 4
70
+ summary: A markdown syntax highlighter
71
+ test_files: []