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.
- checksums.yaml +4 -4
- data/Cargo.lock +20 -9
- data/Cargo.toml +3 -3
- data/README.md +508 -12
- data/lib/mq/query.rb +556 -0
- data/lib/mq.rb +8 -10
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82df60dcb72de275f0c80ef1747279648ea70d46b0dbc971635cd9d86bd7c7a6
|
|
4
|
+
data.tar.gz: f4807fec7ba9bcbab543ffd248e6cda4fa847adae1e699df95571536c6c6010a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
848
|
+
version = "0.6.0"
|
|
849
849
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
850
|
-
checksum = "
|
|
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.
|
|
886
|
+
version = "0.6.0"
|
|
886
887
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
887
|
-
checksum = "
|
|
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.
|
|
897
|
+
version = "0.6.0"
|
|
897
898
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
898
|
-
checksum = "
|
|
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.
|
|
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.
|
|
1555
|
+
version = "0.5.0"
|
|
1555
1556
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1556
|
-
checksum = "
|
|
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.
|
|
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.
|
|
22
|
-
mq-markdown = "0.
|
|
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
|
-
##
|
|
8
|
+
## Installation
|
|
9
9
|
|
|
10
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
36
|
-
puts result.text # => # Hello
|
|
492
|
+
## HTML to Markdown
|
|
37
493
|
|
|
38
|
-
|
|
39
|
-
html = '<h1>Title</h1><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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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:
|
|
105
|
+
rubygems_version: 4.0.10
|
|
105
106
|
specification_version: 4
|
|
106
107
|
summary: Ruby bindings for mq Markdown processing
|
|
107
108
|
test_files: []
|