mq-ruby 0.1.22 → 0.1.23

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.
Files changed (7) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +20 -9
  3. data/Cargo.toml +3 -3
  4. data/README.md +508 -12
  5. data/lib/mq/query.rb +556 -0
  6. data/lib/mq.rb +8 -10
  7. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc8e68991b67c4916d01b6d7fec5a0f7084211e58ca5e43b0b45ac10c5024e53
4
- data.tar.gz: 84a83775b49f9cd44f6c43e15ec91266255199cc960519c1f5a5ad31a0bb5c6b
3
+ metadata.gz: 82df60dcb72de275f0c80ef1747279648ea70d46b0dbc971635cd9d86bd7c7a6
4
+ data.tar.gz: f4807fec7ba9bcbab543ffd248e6cda4fa847adae1e699df95571536c6c6010a
5
5
  SHA512:
6
- metadata.gz: 260de2ebb6141a23dec7406f4fe80203902865996392709b5b64788c305cd903115d5016cb36296dfccba53f3a2f57f20ab21ff356fd84a407bf19de3edc2547
7
- data.tar.gz: 77b526b5c24d7bae085d1c5297aa370882eddfa4c4ad0bf2101d3ac53f78757d2429af55d974e98bc507d6cca39a150a01b5f66109b2c71d8d65ce6fa1723e71
6
+ metadata.gz: e9c91a433c96fa4f7904eae06a5a62fe03ccaf1e29820e39f103172f04ecc5cccb4a87e6e1047cbf5e05b2b13f32c48c9911aaa9fcc99108afb200a2c3c91347
7
+ data.tar.gz: ff45547b349deb0125b9aa3caf9022331b18e0b547b84a83bfe4542eec8296b5a2f4354304a41904899f0b5ac089f782d94974527813573eefa6631e929d5a88
data/Cargo.lock CHANGED
@@ -845,9 +845,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
845
845
 
846
846
  [[package]]
847
847
  name = "mq-lang"
848
- version = "0.5.31"
848
+ version = "0.6.0"
849
849
  source = "registry+https://github.com/rust-lang/crates.io-index"
850
- checksum = "2b51f7340c495c7d3327bec83c45ea26335dae91acb1392cc45ce2fcc2130194"
850
+ checksum = "945450433662b786c8ce51f19ab70891dc442da43d39967c51537927e7f7305a"
851
851
  dependencies = [
852
852
  "base64",
853
853
  "chrono",
@@ -877,14 +877,15 @@ dependencies = [
877
877
  "toml",
878
878
  "toon-format",
879
879
  "url",
880
+ "web-sys",
880
881
  "yaml-rust2",
881
882
  ]
882
883
 
883
884
  [[package]]
884
885
  name = "mq-macros"
885
- version = "0.5.31"
886
+ version = "0.6.0"
886
887
  source = "registry+https://github.com/rust-lang/crates.io-index"
887
- checksum = "d749dc5c73bcce7a6a978636aef01de4700e0e790d88b5a514ae2d5df393cb77"
888
+ checksum = "18f40d74822fe55553384f385363530300ea41ceadd598baf68fe0373bbb1fae"
888
889
  dependencies = [
889
890
  "proc-macro2",
890
891
  "quote",
@@ -893,9 +894,9 @@ dependencies = [
893
894
 
894
895
  [[package]]
895
896
  name = "mq-markdown"
896
- version = "0.5.31"
897
+ version = "0.6.0"
897
898
  source = "registry+https://github.com/rust-lang/crates.io-index"
898
- checksum = "fc96ff6f0490a589fae25145cbc61b883c7fd2d4b042f545eab954c37ed3a167"
899
+ checksum = "791c374af833381678f62652884282f70043110e35b8a4a9f76c72cefca2def2"
899
900
  dependencies = [
900
901
  "ego-tree",
901
902
  "itertools 0.14.0",
@@ -909,7 +910,7 @@ dependencies = [
909
910
 
910
911
  [[package]]
911
912
  name = "mq-ruby"
912
- version = "0.1.22"
913
+ version = "0.1.23"
913
914
  dependencies = [
914
915
  "magnus",
915
916
  "mq-lang",
@@ -1551,9 +1552,9 @@ checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
1551
1552
 
1552
1553
  [[package]]
1553
1554
  name = "toon-format"
1554
- version = "0.4.4"
1555
+ version = "0.5.0"
1555
1556
  source = "registry+https://github.com/rust-lang/crates.io-index"
1556
- checksum = "6d25e33e50b37f95f3b55b6e664218cac7e1a50f056a75bb4c7a6cccfbc8a8c4"
1557
+ checksum = "8f89570c1a68d73941f728cca32a4345b2ffca36667ad921af336c60309a3e7e"
1557
1558
  dependencies = [
1558
1559
  "indexmap",
1559
1560
  "serde",
@@ -1681,6 +1682,16 @@ dependencies = [
1681
1682
  "unicode-ident",
1682
1683
  ]
1683
1684
 
1685
+ [[package]]
1686
+ name = "web-sys"
1687
+ version = "0.3.83"
1688
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1689
+ checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
1690
+ dependencies = [
1691
+ "js-sys",
1692
+ "wasm-bindgen",
1693
+ ]
1694
+
1684
1695
  [[package]]
1685
1696
  name = "web_atoms"
1686
1697
  version = "0.2.3"
data/Cargo.toml CHANGED
@@ -10,7 +10,7 @@ name = "mq-ruby"
10
10
  publish = false
11
11
  readme = "README.md"
12
12
  repository = "https://github.com/harehare/mq"
13
- version = "0.1.22"
13
+ version = "0.1.23"
14
14
 
15
15
  [lib]
16
16
  crate-type = ["cdylib"]
@@ -18,6 +18,6 @@ name = "mq_ruby"
18
18
 
19
19
  [dependencies]
20
20
  magnus = {version = "0.8"}
21
- mq-lang = "0.5.31"
22
- mq-markdown = "0.5.31"
21
+ mq-lang = "0.6.0"
22
+ mq-markdown = "0.6.0"
23
23
 
data/README.md CHANGED
@@ -5,40 +5,536 @@
5
5
 
6
6
  Ruby bindings for [mq](https://mqlang.org/), a jq-like command-line tool for processing Markdown.
7
7
 
8
- ## Ruby API
8
+ ## Installation
9
9
 
10
- Once complete, the Ruby API will look like this:
10
+ Add to your Gemfile:
11
+
12
+ ```ruby
13
+ gem 'mq-ruby'
14
+ ```
15
+
16
+ ## Basic Usage
11
17
 
12
18
  ```ruby
13
19
  require 'mq'
14
20
 
15
- # Basic usage
16
21
  markdown = <<~MD
17
22
  # Main Title
23
+
18
24
  ## Section 1
25
+
19
26
  Some content here.
27
+
20
28
  ## Section 2
29
+
21
30
  More content.
22
31
  MD
23
32
 
33
+ # Run a raw mq query string
24
34
  result = MQ.run('.h2', markdown)
25
- result.values.each do |heading|
26
- puts heading
27
- end
35
+ result.values.each { |h| puts h }
36
+ # => ## Section 1
37
+ # => ## Section 2
38
+
39
+ # Access result as a single string
40
+ puts result.text
28
41
  # => ## Section 1
29
42
  # => ## Section 2
30
43
 
31
- # With options
44
+ # Count matched nodes
45
+ puts result.length # => 2
46
+
47
+ # Index into results (1-based)
48
+ puts result[1] # => ## Section 1
49
+ ```
50
+
51
+ ## Query Builder
52
+
53
+ `MQ::Query` provides a Ruby DSL for building mq query strings programmatically.
54
+ Queries are built by chaining methods and can be passed directly to `MQ.run`.
55
+
56
+ ```ruby
57
+ # Equivalent to MQ.run('.h2', markdown)
58
+ result = MQ.run(MQ::Query.h2, markdown)
59
+
60
+ # Chain with filters and transformations
61
+ query = MQ::Query.h2
62
+ .select { contains("Installation") }
63
+ .to_text
64
+
65
+ result = MQ.run(query, markdown)
66
+ ```
67
+
68
+ ### Selectors
69
+
70
+ #### Heading Selectors
71
+
72
+ ```ruby
73
+ MQ::Query.h1 # .h1 — level-1 headings
74
+ MQ::Query.h2 # .h2 — level-2 headings
75
+ MQ::Query.h3 # .h3
76
+ MQ::Query.h4 # .h4
77
+ MQ::Query.h5 # .h5
78
+ MQ::Query.h6 # .h6
79
+ MQ::Query.heading # .heading — any heading
80
+ ```
81
+
82
+ #### Block Element Selectors
83
+
84
+ ```ruby
85
+ MQ::Query.paragraph # .p
86
+ MQ::Query.code # .code — fenced code blocks
87
+ MQ::Query.blockquote # .blockquote
88
+ MQ::Query.hr # .hr — horizontal rules
89
+ MQ::Query.list # .[] — list items
90
+ MQ::Query.table # .table
91
+ MQ::Query.table_align # .table_align
92
+ MQ::Query.math # .math — math blocks
93
+ MQ::Query.html # .html — raw HTML blocks
94
+ MQ::Query.definition # .definition — link definitions
95
+ MQ::Query.footnote # .footnote
96
+ MQ::Query.toml # .toml — TOML front matter
97
+ MQ::Query.yaml # .yaml — YAML front matter
98
+ ```
99
+
100
+ #### Inline Element Selectors
101
+
102
+ ```ruby
103
+ MQ::Query.text # .text
104
+ MQ::Query.strong # .strong — bold
105
+ MQ::Query.emphasis # .emphasis — italic
106
+ MQ::Query.delete # .delete — strikethrough
107
+ MQ::Query.link # .link
108
+ MQ::Query.image # .image
109
+ MQ::Query.code_inline # .code_inline
110
+ MQ::Query.math_inline # .math_inline
111
+ MQ::Query.link_ref # .link_ref
112
+ MQ::Query.image_ref # .image_ref
113
+ MQ::Query.footnote_ref # .footnote_ref
114
+ MQ::Query.line_break # .break
115
+ ```
116
+
117
+ #### Task List Selectors
118
+
119
+ ```ruby
120
+ MQ::Query.task # .task — any task list item
121
+ MQ::Query.todo # .todo — unchecked task items
122
+ MQ::Query.done # .done — checked task items
123
+ ```
124
+
125
+ #### Indexed Selectors
126
+
127
+ ```ruby
128
+ MQ::Query.list_at(0) # .[0] — first list item
129
+ MQ::Query.list_at(2) # .[2] — third list item
130
+ MQ::Query.table_row(0) # .[0][] — all cells in row 0
131
+ MQ::Query.table_col(1) # .[][1] — all cells in column 1
132
+ MQ::Query.table_cell(0, 1) # .[0][1] — cell at row 0, column 1
133
+ ```
134
+
135
+ #### MDX Selectors
136
+
137
+ ```ruby
138
+ MQ::Query.mdx_jsx_flow_element # .mdx_jsx_flow_element
139
+ MQ::Query.mdx_text_expression # .mdx_text_expression
140
+ MQ::Query.mdx_jsx_text_element # .mdx_jsx_text_element
141
+ MQ::Query.mdx_flow_expression # .mdx_flow_expression
142
+ MQ::Query.mdx_js_esm # .mdx_js_esm
143
+ ```
144
+
145
+ #### Attribute Selectors
146
+
147
+ Access specific attributes of nodes directly:
148
+
149
+ ```ruby
150
+ MQ::Query.code.lang # .code | .lang — language of code blocks
151
+ MQ::Query.link.url # .link | .url — URL of links
152
+ MQ::Query.image.alt # .image | .alt — alt text of images
153
+ MQ::Query.link.title # .link | .title — title of links
154
+
155
+ # All available attribute selectors (class-level)
156
+ MQ::Query.value # .value
157
+ MQ::Query.lang # .lang
158
+ MQ::Query.meta # .meta
159
+ MQ::Query.fence # .fence
160
+ MQ::Query.url # .url
161
+ MQ::Query.alt # .alt
162
+ MQ::Query.depth # .depth — heading depth
163
+ MQ::Query.level # .level
164
+ MQ::Query.ordered # .ordered — list ordered flag
165
+ MQ::Query.checked # .checked — task item checked state
166
+ MQ::Query.column # .column — table cell column index
167
+ MQ::Query.row # .row — table cell row index
168
+ MQ::Query.align # .align — table alignment
169
+ ```
170
+
171
+ #### Instance-level Attribute Access (for chaining)
172
+
173
+ ```ruby
174
+ # After selecting a node, chain attribute selectors:
175
+ MQ::Query.code.lang # .code | .lang
176
+ MQ::Query.link.url # .link | .url
177
+ MQ::Query.heading.depth # .heading | .depth
178
+ MQ::Query.task.checked # .task | .checked
179
+ MQ::Query.list.item_index # .[] | .index (item_index avoids naming conflict)
180
+ MQ::Query.list.ordered # .[] | .ordered
181
+ MQ::Query.table.column # .table | .column
182
+ MQ::Query.table.row # .table | .row
183
+ MQ::Query.table_align.align # .table_align | .align
184
+ MQ::Query.mdx_jsx_flow_element.mdx_name # .mdx_jsx_flow_element | .name
185
+ ```
186
+
187
+ #### Recursive Selector
188
+
189
+ ```ruby
190
+ MQ::Query.recursive # .. — matches all nodes recursively
191
+ ```
192
+
193
+ #### Dict Property Selector
194
+
195
+ ```ruby
196
+ MQ::Query.property("title") # ."title"
197
+ query.property("key") # | ."key"
198
+ ```
199
+
200
+ ### Pipe Operator
201
+
202
+ Chain two queries with `|`:
203
+
204
+ ```ruby
205
+ query = MQ::Query.h2 | MQ::Query.to_text
206
+ # => ".h2 | to_text()"
207
+
208
+ query = MQ::Query.h2 | MQ::Query.select { contains("API") } | MQ::Query.to_text
209
+ # => '.h2 | select(contains("API")) | to_text()'
210
+ ```
211
+
212
+ ### Filtering with `select`
213
+
214
+ ```ruby
215
+ # Block form (recommended)
216
+ MQ::Query.h2.select { contains("Feature") }
217
+ # => '.h2 | select(contains("Feature"))'
218
+
219
+ # Combine conditions with & (and) and | (or)
220
+ MQ::Query.h2.select { contains("API") & starts_with("## ") }
221
+ # => '.h2 | select(contains("API") and starts_with("## "))'
222
+
223
+ MQ::Query.h2.select { contains("A") | contains("B") }
224
+ # => '.h2 | select(contains("A") or contains("B"))'
225
+
226
+ # Negation
227
+ MQ::Query.select { negate(contains("draft")) }
228
+ # => 'select(not(contains("draft")))'
229
+
230
+ # Class-level select (no leading selector)
231
+ MQ::Query.select { is_mdx }
232
+ # => "select(is_mdx())"
233
+
234
+ # String or Filter argument
235
+ MQ::Query.h2.select('contains("Feature")')
236
+ MQ::Query.h2.select(MQ::Filter.new('contains("Feature")'))
237
+ ```
238
+
239
+ ### Mapping with `map`
240
+
241
+ ```ruby
242
+ MQ::Query.list.map { contains("important") }
243
+ # => '.[] | map(contains("important"))'
244
+ ```
245
+
246
+ ### Transformation Methods
247
+
248
+ #### Output
249
+
250
+ ```ruby
251
+ .to_text # to_text() — plain text
252
+ .to_markdown # to_markdown() — markdown string
253
+ .to_mdx # to_mdx() — MDX string
254
+ .to_html # to_html() — HTML string
255
+ .to_string # to_string() — string coercion
256
+ .to_number # to_number() — numeric coercion
257
+ .to_array # to_array()
258
+ .to_bytes # to_bytes()
259
+ .to_markdown_string # to_markdown_string()
260
+ ```
261
+
262
+ #### String Operations
263
+
264
+ ```ruby
265
+ .trim # trim()
266
+ .ltrim # ltrim()
267
+ .rtrim # rtrim()
268
+ .downcase # downcase()
269
+ .upcase # upcase()
270
+ .ascii_downcase # ascii_downcase()
271
+ .ascii_upcase # ascii_upcase()
272
+ .len # len()
273
+ .utf8bytelen # utf8bytelen()
274
+ .explode # explode() — string to codepoints
275
+ .implode # implode() — codepoints to string
276
+ .url_encode # url_encode()
277
+ .intern # intern()
278
+
279
+ .split(",") # split(",")
280
+ .gsub("pat", "r") # gsub("pat", "r") — regex replace all
281
+ .replace("a", "b") # replace("a", "b") — literal replace
282
+ .test("\\d+") # test("\\d+") — regex test → bool
283
+ .capture("(\\w+)") # capture("(\\w+)") — regex capture
284
+ .slice(0, 5) # slice(0, 5)
285
+ .index("sub") # index("sub") — position of substring
286
+ .rindex("sub") # rindex("sub") — last position
287
+ .repeat(3) # repeat(3)
288
+ ```
289
+
290
+ #### Collection Operations
291
+
292
+ ```ruby
293
+ .length # length
294
+ .len # len()
295
+ .add # add
296
+ .first # first
297
+ .last # last
298
+ .empty # empty
299
+ .reverse # reverse
300
+ .sort # sort
301
+ .compact # compact — remove nils
302
+ .uniq # uniq
303
+ .flatten # flatten
304
+ .keys # keys
305
+ .values # values
306
+ .entries # entries
307
+ .children # .children
308
+ .join(", ") # join(", ")
309
+ .nth(2) # nth(2)
310
+ .limit(5) # limit(5)
311
+ .range(3) # range(3)
312
+ .del("key") # del("key")
313
+ .insert(0, "val") # insert(0, "val")
314
+ ```
315
+
316
+ #### Math Operations
317
+
318
+ ```ruby
319
+ .abs # abs()
320
+ .ceil # ceil()
321
+ .floor # floor()
322
+ .round # round()
323
+ .trunc # trunc()
324
+ .sqrt # sqrt()
325
+ .ln # ln()
326
+ .log10 # log10()
327
+ .exp # exp()
328
+ .pow(2) # pow(2)
329
+ .min(0) # min(0)
330
+ .max(100) # max(100)
331
+ .negate_val # negate() — numeric negation
332
+ .is_nan # is_nan()
333
+ ```
334
+
335
+ #### Type / Logic
336
+
337
+ ```ruby
338
+ .type # type
339
+ .coalesce("default") # coalesce("default")
340
+ .debug # debug
341
+ ```
342
+
343
+ #### Encoding
344
+
345
+ ```ruby
346
+ .base64 # base64()
347
+ .base64d # base64d()
348
+ .base64url # base64url()
349
+ .base64urld # base64urld()
350
+ .md5 # md5()
351
+ .sha256 # sha256()
352
+ .sha512 # sha512()
353
+ .from_hex # from_hex()
354
+ .to_hex # to_hex()
355
+ ```
356
+
357
+ #### Path Operations
358
+
359
+ ```ruby
360
+ .basename # basename()
361
+ .dirname # dirname()
362
+ .extname # extname()
363
+ .stem # stem()
364
+ .path_join("sub") # path_join("sub")
365
+ ```
366
+
367
+ #### Dict Operations
368
+
369
+ ```ruby
370
+ .get("key") # get("key")
371
+ .set("key", "val") # set("key", "val")
372
+ .property("key") # ."key"
373
+ ```
374
+
375
+ #### Markdown Attribute Operations
376
+
377
+ ```ruby
378
+ .update("New content") # update("New content")
379
+ .attr("lang") # attr("lang")
380
+ .set_attr("lang", "ruby") # set_attr("lang", "ruby")
381
+ .get_title # get_title
382
+ .get_url # get_url
383
+ .set_check(true) # set_check(true)
384
+ .set_ref("myref") # set_ref("myref")
385
+ .set_code_block_lang("python") # set_code_block_lang("python")
386
+ .set_list_ordered(true) # set_list_ordered(true)
387
+ ```
388
+
389
+ #### Markdown Construction
390
+
391
+ ```ruby
392
+ .to_code("ruby") # to_code("ruby")
393
+ .to_code # to_code(null) — no language
394
+ .to_code_inline # to_code_inline()
395
+ .to_h(2) # to_h(2) — convert to heading level 2
396
+ .to_hr # to_hr()
397
+ .to_link("url", "text", "title") # to_link(...)
398
+ .to_link("url", "text") # to_link(...) — empty title
399
+ .to_link("url") # to_link(...) — current value as text
400
+ .to_image("url", "alt", "title") # to_image(...)
401
+ .to_math # to_math()
402
+ .to_math_inline # to_math_inline()
403
+ .to_strong # to_strong()
404
+ .to_em # to_em()
405
+ .to_md_text # to_md_text()
406
+ .to_md_list(0) # to_md_list(0) — nesting level
407
+ .to_md_name("component") # to_md_name("component")
408
+ .to_md_table_row("A", "B", "C") # to_md_table_row(...)
409
+ .to_md_table_cell("val", 0, 1) # to_md_table_cell(...)
410
+ ```
411
+
412
+ ### Filter DSL
413
+
414
+ All filter methods return a `MQ::Filter` that can be combined with `&` (and) and `|` (or).
415
+
416
+ #### String Matching
417
+
418
+ ```ruby
419
+ contains("text") # contains("text")
420
+ starts_with("## ") # starts_with("## ")
421
+ ends_with(".") # ends_with(".")
422
+ test("\\d+") # test("\\d+") — regex test
423
+ ```
424
+
425
+ #### Regex
426
+
427
+ ```ruby
428
+ is_regex_match("\\d+") # is_regex_match("\\d+")
429
+ is_not_regex_match("\\d+") # is_not_regex_match("\\d+")
430
+ ```
431
+
432
+ #### Comparison Operators
433
+
434
+ These compare the current pipeline value against the argument:
435
+
436
+ ```ruby
437
+ eq("value") # eq("value") — equal
438
+ ne("value") # ne("value") — not equal
439
+ gt(5) # gt(5) — greater than
440
+ gte(5) # gte(5) — greater than or equal
441
+ lt(5) # lt(5) — less than
442
+ lte(5) # lte(5) — less than or equal
443
+ ```
444
+
445
+ #### Type Checks
446
+
447
+ ```ruby
448
+ is_mdx # is_mdx()
449
+ is_none # is_none()
450
+ is_nan # is_nan()
451
+ type # type
452
+ ```
453
+
454
+ #### Other
455
+
456
+ ```ruby
457
+ negate(contains("draft")) # not(contains("draft"))
458
+ length # length
459
+ empty # empty
460
+ add # add
461
+ ```
462
+
463
+ ### Combining Filters
464
+
465
+ ```ruby
466
+ MQ::Query.h2.select { contains("API") & negate(contains("Internal")) }
467
+ # => '.h2 | select(contains("API") and not(contains("Internal")))'
468
+
469
+ MQ::Query.h2.select { starts_with("## ") | ends_with("!") }
470
+ # => '.h2 | select(starts_with("## ") or ends_with("!"))'
471
+
472
+ # Three-way AND
473
+ MQ::Query.h2.select {
474
+ contains("API") & negate(contains("Internal")) & starts_with("## ")
475
+ }
476
+ ```
477
+
478
+ ## Options
479
+
480
+ ```ruby
32
481
  options = MQ::Options.new
482
+ options.input_format = MQ::InputFormat::MARKDOWN # default
483
+ options.input_format = MQ::InputFormat::MDX
484
+ options.input_format = MQ::InputFormat::TEXT
33
485
  options.input_format = MQ::InputFormat::HTML
486
+ options.input_format = MQ::InputFormat::RAW
487
+ options.input_format = MQ::InputFormat::NULL
488
+
489
+ result = MQ.run('.h1', content, options)
490
+ ```
34
491
 
35
- result = MQ.run('.h1', '<h1>Hello</h1><p>World</p>', options)
36
- puts result.text # => # Hello
492
+ ## HTML to Markdown
37
493
 
38
- # HTML to Markdown conversion
39
- html = '<h1>Title</h1><p>Paragraph</p>'
494
+ ```ruby
495
+ html = '<h1>Title</h1><p>This is a <strong>test</strong>.</p>'
40
496
  markdown = MQ.html_to_markdown(html)
41
- puts markdown # => # Title\n\nParagraph
497
+ # => "# Title\n\nThis is a **test**."
498
+
499
+ # With conversion options
500
+ options = MQ::ConversionOptions.new
501
+ options.use_title_as_h1 = true
502
+ options.extract_scripts_as_code_blocks = true
503
+ options.generate_front_matter = true
504
+
505
+ markdown = MQ.html_to_markdown(html, options)
506
+ ```
507
+
508
+ ## Examples
509
+
510
+ ```ruby
511
+ require 'mq'
512
+
513
+ content = File.read('README.md')
514
+
515
+ # Extract all h2 headings containing "API"
516
+ MQ.run(MQ::Query.h2.select { contains("API") }, content).values
517
+
518
+ # Get all code block languages used
519
+ MQ.run(MQ::Query.code.lang, content).values
520
+
521
+ # Get all link URLs
522
+ MQ.run(MQ::Query.link.url, content).values
523
+
524
+ # Extract headings as plain text (no # prefix)
525
+ MQ.run(MQ::Query.h2.to_text, content).values
526
+
527
+ # Find unchecked task items
528
+ MQ.run(MQ::Query.todo, content).values
529
+
530
+ # Get the first list item
531
+ MQ.run(MQ::Query.list_at(0), content).values
532
+
533
+ # Count h2 headings
534
+ MQ.run(MQ::Query.h2.length, content).values
535
+
536
+ # Extract YAML front matter
537
+ MQ.run(MQ::Query.yaml, content).values
42
538
  ```
43
539
 
44
540
  ## License
data/lib/mq/query.rb ADDED
@@ -0,0 +1,556 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQ
4
+ # Programmatic query builder for constructing mq queries in Ruby.
5
+ #
6
+ # @example Basic selector
7
+ # MQ::Query.h2
8
+ # # => ".h2"
9
+ #
10
+ # @example Selector with filter
11
+ # MQ::Query.h2.select { contains("Feature") }
12
+ # # => '.h2 | select(contains("Feature"))'
13
+ #
14
+ # @example Pipe operator
15
+ # MQ::Query.h2 | MQ::Query.to_text
16
+ # # => ".h2 | to_text()"
17
+ #
18
+ # @example Attribute access
19
+ # MQ::Query.link.url
20
+ # # => ".link | .url"
21
+ #
22
+ # @example Complex chain
23
+ # MQ::Query.h2
24
+ # .select { contains("Section") & starts_with("##") }
25
+ # .to_text
26
+ # # => '.h2 | select(contains("Section") and starts_with("##")) | to_text()'
27
+ #
28
+ # @example Using with MQ.run
29
+ # result = MQ.run(MQ::Query.h2.select { contains("Feature") }, content)
30
+ class Query
31
+ def initialize(expr = "")
32
+ @expr = expr.to_s
33
+ end
34
+
35
+ # Pipe two queries together using the | operator.
36
+ #
37
+ # @param other [Query, #to_query] the query to pipe into
38
+ # @return [Query]
39
+ def |(other)
40
+ self.class.new("#{@expr} | #{other.to_query}")
41
+ end
42
+
43
+ # Append a select() filter.
44
+ #
45
+ # @param filter [Filter, String, nil] filter expression (or use block)
46
+ # @yield block evaluated in {FilterDSL} context
47
+ # @return [Query]
48
+ def select(filter = nil, &block)
49
+ filter_str = resolve_filter(filter, &block)
50
+ pipe_with("select(#{filter_str})")
51
+ end
52
+
53
+ # Append a map() transformation.
54
+ #
55
+ # @param filter [Filter, String, nil] filter expression (or use block)
56
+ # @yield block evaluated in {FilterDSL} context
57
+ # @return [Query]
58
+ def map(filter = nil, &block)
59
+ filter_str = resolve_filter(filter, &block)
60
+ pipe_with("map(#{filter_str})")
61
+ end
62
+
63
+ def to_text = pipe_with("to_text()")
64
+ def to_markdown = pipe_with("to_markdown()")
65
+ def to_mdx = pipe_with("to_mdx()")
66
+ def to_html = pipe_with("to_html()")
67
+ def to_string = pipe_with("to_string()")
68
+ def to_number = pipe_with("to_number()")
69
+ def to_array = pipe_with("to_array()")
70
+ def to_bytes = pipe_with("to_bytes()")
71
+ def to_markdown_string = pipe_with("to_markdown_string()")
72
+
73
+ def length = pipe_with("length")
74
+ def len = pipe_with("len()")
75
+ def utf8bytelen = pipe_with("utf8bytelen()")
76
+ def add = pipe_with("add")
77
+ def first = pipe_with("first")
78
+ def last = pipe_with("last")
79
+ def empty = pipe_with("empty")
80
+ def reverse = pipe_with("reverse")
81
+ def sort = pipe_with("sort")
82
+ def compact = pipe_with("compact")
83
+ def uniq = pipe_with("uniq")
84
+ def flatten = pipe_with("flatten")
85
+ def keys = pipe_with("keys")
86
+ def values = pipe_with("values")
87
+ def entries = pipe_with("entries")
88
+ def children = pipe_with(".children")
89
+
90
+ def split(separator)
91
+ pipe_with("split(#{separator.inspect})")
92
+ end
93
+
94
+ def join(separator)
95
+ pipe_with("join(#{separator.inspect})")
96
+ end
97
+
98
+ def nth(n)
99
+ pipe_with("nth(#{n})")
100
+ end
101
+
102
+ def limit(n)
103
+ pipe_with("limit(#{n})")
104
+ end
105
+
106
+ def range(n)
107
+ pipe_with("range(#{n})")
108
+ end
109
+
110
+ def slice(start, stop)
111
+ pipe_with("slice(#{start}, #{stop})")
112
+ end
113
+
114
+ def index(value)
115
+ pipe_with("index(#{value.inspect})")
116
+ end
117
+
118
+ def rindex(value)
119
+ pipe_with("rindex(#{value.inspect})")
120
+ end
121
+
122
+ def del(value)
123
+ pipe_with("del(#{value.inspect})")
124
+ end
125
+
126
+ def insert(idx, val)
127
+ pipe_with("insert(#{idx}, #{val.inspect})")
128
+ end
129
+
130
+ def repeat(n)
131
+ pipe_with("repeat(#{n})")
132
+ end
133
+
134
+ def trim = pipe_with("trim()")
135
+ def ltrim = pipe_with("ltrim()")
136
+ def rtrim = pipe_with("rtrim()")
137
+ def downcase = pipe_with("downcase()")
138
+ def upcase = pipe_with("upcase()")
139
+ def ascii_downcase = pipe_with("ascii_downcase()")
140
+ def ascii_upcase = pipe_with("ascii_upcase()")
141
+ def explode = pipe_with("explode()")
142
+ def implode = pipe_with("implode()")
143
+ def url_encode = pipe_with("url_encode()")
144
+ def intern = pipe_with("intern()")
145
+
146
+ def gsub(pattern, replacement)
147
+ pipe_with("gsub(#{pattern.inspect}, #{replacement.inspect})")
148
+ end
149
+
150
+ def replace(from, to)
151
+ pipe_with("replace(#{from.inspect}, #{to.inspect})")
152
+ end
153
+
154
+ def test(pattern)
155
+ pipe_with("test(#{pattern.inspect})")
156
+ end
157
+
158
+ def capture(pattern)
159
+ pipe_with("capture(#{pattern.inspect})")
160
+ end
161
+
162
+ def abs = pipe_with("abs()")
163
+ def ceil = pipe_with("ceil()")
164
+ def floor = pipe_with("floor()")
165
+ def round = pipe_with("round()")
166
+ def trunc = pipe_with("trunc()")
167
+ def sqrt = pipe_with("sqrt()")
168
+ def ln = pipe_with("ln()")
169
+ def log10 = pipe_with("log10()")
170
+ def exp = pipe_with("exp()")
171
+ def negate_val = pipe_with("negate()")
172
+ def is_nan = pipe_with("is_nan()")
173
+
174
+ def pow(n)
175
+ pipe_with("pow(#{n})")
176
+ end
177
+
178
+ def min(other)
179
+ pipe_with("min(#{other})")
180
+ end
181
+
182
+ def max(other)
183
+ pipe_with("max(#{other})")
184
+ end
185
+
186
+ # --- Type / logic ---
187
+
188
+ def type = pipe_with("type")
189
+ def debug = pipe_with("debug")
190
+
191
+ def coalesce(default)
192
+ pipe_with("coalesce(#{default.inspect})")
193
+ end
194
+
195
+ def base64 = pipe_with("base64()")
196
+ def base64d = pipe_with("base64d()")
197
+ def base64url = pipe_with("base64url()")
198
+ def base64urld = pipe_with("base64urld()")
199
+ def md5 = pipe_with("md5()")
200
+ def sha256 = pipe_with("sha256()")
201
+ def sha512 = pipe_with("sha512()")
202
+ def from_hex = pipe_with("from_hex()")
203
+ def to_hex = pipe_with("to_hex()")
204
+ def to_hex_str = pipe_with("to_hex()")
205
+
206
+ def basename = pipe_with("basename()")
207
+ def dirname = pipe_with("dirname()")
208
+ def extname = pipe_with("extname()")
209
+ def stem = pipe_with("stem()")
210
+
211
+ def path_join(other)
212
+ pipe_with("path_join(#{other.inspect})")
213
+ end
214
+
215
+ def get(key)
216
+ pipe_with("get(#{key.inspect})")
217
+ end
218
+
219
+ def set(key, val)
220
+ pipe_with("set(#{key.inspect}, #{val.inspect})")
221
+ end
222
+
223
+ # Access a dict property by key (generates ."key" selector)
224
+ def property(key)
225
+ pipe_with(".\"#{key}\"")
226
+ end
227
+
228
+ # Attribute selectors (access attributes of selected nodes)
229
+ # These generate attribute selector syntax (.url, .lang, etc.)
230
+
231
+ def value = pipe_with(".value")
232
+ def lang = pipe_with(".lang")
233
+ def meta = pipe_with(".meta")
234
+ def fence = pipe_with(".fence")
235
+ def url = pipe_with(".url")
236
+ def alt = pipe_with(".alt")
237
+ def title = pipe_with(".title")
238
+ def ident = pipe_with(".ident")
239
+ def label = pipe_with(".label")
240
+ def depth = pipe_with(".depth")
241
+ def level = pipe_with(".level")
242
+ def item_index = pipe_with(".index")
243
+ def ordered = pipe_with(".ordered")
244
+ def checked = pipe_with(".checked")
245
+ def column = pipe_with(".column")
246
+ def row = pipe_with(".row")
247
+ def align = pipe_with(".align")
248
+ def mdx_name = pipe_with(".name")
249
+
250
+ def update(content)
251
+ pipe_with("update(#{content.inspect})")
252
+ end
253
+
254
+ def attr(name)
255
+ pipe_with("attr(#{name.inspect})")
256
+ end
257
+
258
+ def set_attr(name, val)
259
+ pipe_with("set_attr(#{name.inspect}, #{val.inspect})")
260
+ end
261
+
262
+ def get_title = pipe_with("get_title")
263
+ def get_url = pipe_with("get_url")
264
+
265
+ def set_check(val)
266
+ pipe_with("set_check(#{val})")
267
+ end
268
+
269
+ def set_ref(ref)
270
+ pipe_with("set_ref(#{ref.inspect})")
271
+ end
272
+
273
+ def set_code_block_lang(lang)
274
+ pipe_with("set_code_block_lang(#{lang.inspect})")
275
+ end
276
+
277
+ def set_list_ordered(val)
278
+ pipe_with("set_list_ordered(#{val})")
279
+ end
280
+
281
+ # Convert current value to a code block with the given language.
282
+ def to_code(lang = nil)
283
+ lang ? pipe_with("to_code(#{lang.inspect})") : pipe_with("to_code(null)")
284
+ end
285
+
286
+ def to_code_inline = pipe_with("to_code_inline()")
287
+
288
+ # Convert current value to a heading of the given depth (1-6).
289
+ def to_h(depth)
290
+ pipe_with("to_h(#{depth})")
291
+ end
292
+
293
+ def to_hr = pipe_with("to_hr()")
294
+
295
+ # Create a link node. With all three args no auto-prepend occurs.
296
+ # With two args the current value becomes the link text.
297
+ def to_link(url, text = nil, link_title = "")
298
+ if text
299
+ pipe_with("to_link(#{url.inspect}, #{text.inspect}, #{link_title.inspect})")
300
+ else
301
+ pipe_with("to_link(#{url.inspect}, #{link_title.inspect})")
302
+ end
303
+ end
304
+
305
+ # Create an image node. With all three args no auto-prepend occurs.
306
+ # With two args the current value becomes the alt text.
307
+ def to_image(url, img_alt = nil, img_title = "")
308
+ if img_alt
309
+ pipe_with("to_image(#{url.inspect}, #{img_alt.inspect}, #{img_title.inspect})")
310
+ else
311
+ pipe_with("to_image(#{url.inspect}, #{img_title.inspect})")
312
+ end
313
+ end
314
+
315
+ def to_math = pipe_with("to_math()")
316
+ def to_math_inline = pipe_with("to_math_inline()")
317
+ def to_strong = pipe_with("to_strong()")
318
+ def to_em = pipe_with("to_em()")
319
+ def to_md_text = pipe_with("to_md_text()")
320
+
321
+ # Convert current value to a list item at the given nesting level.
322
+ def to_md_list(list_level)
323
+ pipe_with("to_md_list(#{list_level})")
324
+ end
325
+
326
+ # Convert current value to a markdown element with the given node name.
327
+ def to_md_name(node_name)
328
+ pipe_with("to_md_name(#{node_name.inspect})")
329
+ end
330
+
331
+ # Build a table row from the given cell values.
332
+ def to_md_table_row(*cells)
333
+ pipe_with("to_md_table_row(#{cells.map(&:inspect).join(', ')})")
334
+ end
335
+
336
+ # Build a table cell with content, row index, and column index.
337
+ def to_md_table_cell(content, r, c)
338
+ pipe_with("to_md_table_cell(#{content.inspect}, #{r}, #{c})")
339
+ end
340
+
341
+ # Returns the mq query string.
342
+ # @return [String]
343
+ def to_query = @expr
344
+ alias to_s to_query
345
+
346
+ class << self
347
+ # --- Heading selectors: h1 through h6 ---
348
+ (1..6).each { |n| define_method("h#{n}") { new(".h#{n}") } }
349
+
350
+ # Generic heading (any level)
351
+ def heading = new(".heading")
352
+
353
+ # Block element selectors
354
+ def code = new(".code")
355
+ def paragraph = new(".p")
356
+ def blockquote = new(".blockquote")
357
+ def hr = new(".hr")
358
+ def image = new(".image")
359
+ def link = new(".link")
360
+ def text = new(".text")
361
+ def strong = new(".strong")
362
+ def emphasis = new(".emphasis")
363
+ def delete = new(".delete")
364
+ def math = new(".math")
365
+ def table = new(".table")
366
+ def table_align = new(".table_align")
367
+ def html = new(".html")
368
+ def definition = new(".definition")
369
+ def footnote = new(".footnote")
370
+ def toml = new(".toml")
371
+ def yaml = new(".yaml")
372
+
373
+ # Inline element selectors
374
+ def code_inline = new(".code_inline")
375
+ def math_inline = new(".math_inline")
376
+ def link_ref = new(".link_ref")
377
+ def image_ref = new(".image_ref")
378
+ def footnote_ref = new(".footnote_ref")
379
+ def line_break = new(".break")
380
+
381
+ # Task list selectors
382
+ def task = new(".task")
383
+ def todo = new(".todo")
384
+ def done = new(".done")
385
+
386
+ # --- List selector ---
387
+ def list = new(".[]")
388
+
389
+ # List item at a specific index: .[n]
390
+ def list_at(n)
391
+ new(".[#{n}]")
392
+ end
393
+
394
+ # --- Table selectors with row/column indexing ---
395
+
396
+ # All cells in a specific row: .[n][]
397
+ def table_row(n)
398
+ new(".[#{n}][]")
399
+ end
400
+
401
+ # All cells in a specific column: .[][n]
402
+ def table_col(n)
403
+ new(".[][#{n}]")
404
+ end
405
+
406
+ # A specific cell: .[row][col]
407
+ def table_cell(r, c)
408
+ new(".[#{r}][#{c}]")
409
+ end
410
+
411
+ # --- MDX selectors ---
412
+ def mdx_jsx_flow_element = new(".mdx_jsx_flow_element")
413
+ def mdx_text_expression = new(".mdx_text_expression")
414
+ def mdx_jsx_text_element = new(".mdx_jsx_text_element")
415
+ def mdx_flow_expression = new(".mdx_flow_expression")
416
+ def mdx_js_esm = new(".mdx_js_esm")
417
+
418
+ # Recursive / deep selector (..)
419
+ def recursive = new("..")
420
+
421
+ # --- Attribute selectors (as standalone starting points) ---
422
+ def value = new(".value")
423
+ def node_values = new(".values")
424
+ def lang = new(".lang")
425
+ def meta = new(".meta")
426
+ def fence = new(".fence")
427
+ def url = new(".url")
428
+ def alt = new(".alt")
429
+ def depth = new(".depth")
430
+ def level = new(".level")
431
+ def ordered = new(".ordered")
432
+ def checked = new(".checked")
433
+ def column = new(".column")
434
+ def row = new(".row")
435
+ def align = new(".align")
436
+
437
+ # Dict property selector: ."key"
438
+ def property(key)
439
+ new(".\"#{key}\"")
440
+ end
441
+
442
+ # Class-level select (no leading selector)
443
+ #
444
+ # @param filter [Filter, String, nil]
445
+ # @yield block evaluated in {FilterDSL} context
446
+ # @return [Query]
447
+ def select(filter = nil, &block)
448
+ filter_str = new.send(:resolve_filter, filter, &block)
449
+ new("select(#{filter_str})")
450
+ end
451
+
452
+ def to_text = new("to_text()")
453
+ def to_markdown = new("to_markdown()")
454
+ end
455
+
456
+ private
457
+
458
+ def pipe_with(expr)
459
+ @expr.empty? ? self.class.new(expr) : self.class.new("#{@expr} | #{expr}")
460
+ end
461
+
462
+ def resolve_filter(filter, &block)
463
+ if block_given?
464
+ Filter.build(&block)
465
+ elsif filter.respond_to?(:to_query)
466
+ filter.to_query
467
+ else
468
+ filter.to_s
469
+ end
470
+ end
471
+ end
472
+
473
+ # Represents a boolean filter expression for use inside select() and map().
474
+ #
475
+ # Filters can be combined with & (and) and | (or):
476
+ # contains("foo") & starts_with("bar")
477
+ # # => 'contains("foo") and starts_with("bar")'
478
+ class Filter
479
+ def initialize(expr)
480
+ @expr = expr.to_s
481
+ end
482
+
483
+ # Build a filter expression by evaluating a block in {FilterDSL} context.
484
+ # @yield block in FilterDSL context
485
+ # @return [String]
486
+ def self.build(&block)
487
+ result = FilterDSL.new.instance_eval(&block)
488
+ result.respond_to?(:to_filter) ? result.to_filter : result.to_s
489
+ end
490
+
491
+ # Combine two filters with boolean AND.
492
+ def &(other) = self.class.new("#{@expr} and #{other}")
493
+
494
+ # Combine two filters with boolean OR.
495
+ def |(other) = self.class.new("#{@expr} or #{other}")
496
+
497
+ def to_filter = @expr
498
+ alias to_query to_filter
499
+ alias to_s to_filter
500
+ end
501
+
502
+ # DSL context for building filter expressions inside select/map blocks.
503
+ #
504
+ # All methods return a {Filter} that can be combined with & and |.
505
+ #
506
+ # @example String matching
507
+ # MQ::Query.h2.select { contains("Feature") & starts_with("##") }
508
+ #
509
+ # @example Comparison
510
+ # MQ::Query.list.select { gt(5) }
511
+ #
512
+ # @example Negation
513
+ # MQ::Query.select { negate(contains("draft")) }
514
+ class FilterDSL
515
+ # String matching
516
+ def contains(text) = Filter.new("contains(#{text.inspect})")
517
+ def starts_with(text) = Filter.new("starts_with(#{text.inspect})")
518
+ def ends_with(text) = Filter.new("ends_with(#{text.inspect})")
519
+ def test(pattern) = Filter.new("test(#{pattern.inspect})")
520
+
521
+ # Regex matching
522
+ def is_regex_match(pattern) = Filter.new("is_regex_match(#{pattern.inspect})")
523
+ def is_not_regex_match(pattern) = Filter.new("is_not_regex_match(#{pattern.inspect})")
524
+
525
+ # Comparison operators
526
+ # These compare the current pipeline value against the given argument.
527
+ def eq(value) = Filter.new("eq(#{value.inspect})")
528
+ def ne(value) = Filter.new("ne(#{value.inspect})")
529
+ def gt(value) = Filter.new("gt(#{value.inspect})")
530
+ def gte(value) = Filter.new("gte(#{value.inspect})")
531
+ def lt(value) = Filter.new("lt(#{value.inspect})")
532
+ def lte(value) = Filter.new("lte(#{value.inspect})")
533
+
534
+ # Type checks
535
+ def is_mdx = Filter.new("is_mdx()")
536
+ def is_none = Filter.new("is_none()")
537
+ def is_nan = Filter.new("is_nan()")
538
+ def type = Filter.new("type")
539
+
540
+ # String transforms usable in filter context
541
+ def length = Filter.new("length")
542
+ def ascii_downcase = Filter.new("ascii_downcase()")
543
+ def ascii_upcase = Filter.new("ascii_upcase()")
544
+ def trim = Filter.new("trim()")
545
+ def empty = Filter.new("empty")
546
+ def add = Filter.new("add")
547
+
548
+ # Negate a filter expression with not().
549
+ # Use +negate+ instead of +not+ since +not+ is a Ruby keyword.
550
+ #
551
+ # @example
552
+ # MQ::Query.select { negate(contains("draft")) }
553
+ # # => 'select(not(contains("draft")))'
554
+ def negate(filter) = Filter.new("not(#{filter})")
555
+ end
556
+ end
data/lib/mq.rb CHANGED
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- # Try to load the compiled extension
5
- RUBY_VERSION =~ /(\d+\.\d+)/
6
- require_relative "mq/#{Regexp.last_match(1)}/mq_ruby"
7
- rescue LoadError
8
- require_relative "mq/mq_ruby"
9
- end
3
+ require_relative "mq/mq_ruby"
4
+
5
+ require_relative "mq/query"
10
6
 
11
7
  module MQ
12
8
  class Error < StandardError; end
@@ -46,15 +42,17 @@ module MQ
46
42
  end
47
43
 
48
44
  class << self
49
- # Run an mq query on the provided content
45
+ # Run an mq query on the provided content.
46
+ # Accepts either a query string or a {Query} object.
50
47
  #
51
- # @param code [String] The mq query string
48
+ # @param code [String, Query] The mq query string or Query builder object
52
49
  # @param content [String] The markdown/HTML/text content to process
53
50
  # @param options [Options, nil] Optional configuration options
54
51
  # @return [Result] The query results
55
52
  def run(code, content, options = nil)
53
+ query = code.respond_to?(:to_query) ? code.to_query : code
56
54
  options_hash = options&.to_h
57
- _run(code, content, options_hash)
55
+ _run(query, content, options_hash)
58
56
  end
59
57
 
60
58
  # Convert HTML to Markdown
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mq-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.22
4
+ version: 0.1.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Takahiro Sato
@@ -78,6 +78,7 @@ files:
78
78
  - README.md
79
79
  - extconf.rb
80
80
  - lib/mq.rb
81
+ - lib/mq/query.rb
81
82
  - src/lib.rs
82
83
  - src/result.rs
83
84
  - src/value.rs
@@ -94,14 +95,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
95
  requirements:
95
96
  - - ">="
96
97
  - !ruby/object:Gem::Version
97
- version: 3.0.0
98
+ version: '3.1'
98
99
  required_rubygems_version: !ruby/object:Gem::Requirement
99
100
  requirements:
100
101
  - - ">="
101
102
  - !ruby/object:Gem::Version
102
103
  version: '0'
103
104
  requirements: []
104
- rubygems_version: 3.6.9
105
+ rubygems_version: 4.0.10
105
106
  specification_version: 4
106
107
  summary: Ruby bindings for mq Markdown processing
107
108
  test_files: []