maquina_remend 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 +7 -0
- data/.rdoc_options +16 -0
- data/CHANGELOG.md +31 -0
- data/LICENSE.txt +21 -0
- data/README.md +169 -0
- data/docs/handlers.md +223 -0
- data/docs/repairs.md +391 -0
- data/docs/streaming.md +162 -0
- data/lib/maquina_remend/context.rb +86 -0
- data/lib/maquina_remend/handlers/app_tags.rb +140 -0
- data/lib/maquina_remend/handlers/base.rb +109 -0
- data/lib/maquina_remend/handlers/comparison_operators.rb +33 -0
- data/lib/maquina_remend/handlers/dangling_escape.rb +45 -0
- data/lib/maquina_remend/handlers/emphasis.rb +117 -0
- data/lib/maquina_remend/handlers/html_tags.rb +34 -0
- data/lib/maquina_remend/handlers/inline_code.rb +22 -0
- data/lib/maquina_remend/handlers/links.rb +79 -0
- data/lib/maquina_remend/handlers/math.rb +43 -0
- data/lib/maquina_remend/handlers/setext_heading.rb +46 -0
- data/lib/maquina_remend/handlers/single_tilde.rb +28 -0
- data/lib/maquina_remend/handlers/strikethrough.rb +25 -0
- data/lib/maquina_remend/pipeline.rb +123 -0
- data/lib/maquina_remend/scanner.rb +187 -0
- data/lib/maquina_remend/version.rb +8 -0
- data/lib/maquina_remend.rb +255 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3ae80dab37d601a1b8570d73e4d29e2f10296205ae349cdace4bfe2f8d17cb53
|
|
4
|
+
data.tar.gz: b97c54a013f0c2dc60616bc1af16ecd3a66786dacf162535c47ed2ec8976ea93
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: fc6b594c251f168ac266659d123d3fddf3a43c10b3b7470a076b3496c75ee9477f86da915634e9aa32b9964fe338b4d0d8039e7398328db1ed3ab15af03d384b
|
|
7
|
+
data.tar.gz: 3fa25bb6cb8cb511338fafbd7261115a52a4f8203a7c91ef2ea293bb3bf595b43ae2b4c2e79078749d429e59a7baff824b4a7cb4d7931a22be46aeb99349d667
|
data/.rdoc_options
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# RDoc configuration. Comments in this gem are written in Markdown, not RDoc
|
|
2
|
+
# markup, so `markup` has to be set here as well as in the gemspec's
|
|
3
|
+
# rdoc_options: `gem rdoc` reads this file, rubygems.org's doc build reads the
|
|
4
|
+
# gemspec, and a bare `rdoc` in a checkout reads only this one.
|
|
5
|
+
---
|
|
6
|
+
markup: markdown
|
|
7
|
+
title: maquina_remend
|
|
8
|
+
main_page: README.md
|
|
9
|
+
exclude:
|
|
10
|
+
- "/sdd/"
|
|
11
|
+
- "CLAUDE\\.md$"
|
|
12
|
+
- "/test/"
|
|
13
|
+
- "remend-patterns\\.md$"
|
|
14
|
+
- "Gemfile"
|
|
15
|
+
- "Rakefile$"
|
|
16
|
+
- "\\.gemspec$"
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-09-07
|
|
4
|
+
|
|
5
|
+
First working version. `MaquinaRemend.call` repairs the tail of a markdown
|
|
6
|
+
buffer that is still arriving, so a renderer never sees a broken document
|
|
7
|
+
mid-stream.
|
|
8
|
+
|
|
9
|
+
- Fourteen completion cases: emphasis, strong, inline code, links and images,
|
|
10
|
+
block math, setext headings, HTML tags, strikethrough, a dangling escape.
|
|
11
|
+
- Application tags — `<thinking>`, `<answer>`, `<tool_call>`, `<citation>`,
|
|
12
|
+
`<scratchpad>` — closed while the model is still inside them, so the answer
|
|
13
|
+
that follows a blank line does not escape the tag. `app_tags:` takes the list
|
|
14
|
+
of names, or `false`.
|
|
15
|
+
- Nine guard cases, including comparison operators and single tildes that must
|
|
16
|
+
not be mistaken for markup.
|
|
17
|
+
- A scanner that tracks fence, inline-code and math state, so a repair never
|
|
18
|
+
fires inside a code block.
|
|
19
|
+
- Per-handler options and a custom handler contract, with `Context` exposing
|
|
20
|
+
the state a handler needs to decide for itself.
|
|
21
|
+
- Idempotent, and byte-identical on well-formed input. Both are asserted over
|
|
22
|
+
the whole corpus, at every truncation point of every document.
|
|
23
|
+
- Application-meaning tags a model leaves open — `<thinking>`, `<answer>`,
|
|
24
|
+
`<tool_call>` and any name you configure — are closed, so a renderer is never
|
|
25
|
+
handed a document whose AST and DOM disagree about where the tag ends.
|
|
26
|
+
- Zero runtime dependencies and no Rails, asserted in a bare
|
|
27
|
+
`ruby --disable-gems` process.
|
|
28
|
+
|
|
29
|
+
Verified against 24 real agent transcripts (14,434 stream chunks), which is how
|
|
30
|
+
the dangling-escape case was found: a trailing backslash escapes whatever the
|
|
31
|
+
next handler appends, so the repair grew on every pass.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mario Alberto Chávez
|
|
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,169 @@
|
|
|
1
|
+
# maquina_remend
|
|
2
|
+
|
|
3
|
+
Repairs the tail of a markdown buffer that is still being streamed.
|
|
4
|
+
|
|
5
|
+
A model emits markdown a token at a time. Rendered naively, the tail of the
|
|
6
|
+
buffer is broken for most of the message's life. The model has typed `**bol`
|
|
7
|
+
and the reader sees two asterisks; it has typed `[the guide](https://exa` and
|
|
8
|
+
the reader sees a link to the wrong host. `maquina_remend` completes that tail,
|
|
9
|
+
so the renderer always receives a well-formed document.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
MaquinaRemend.call("**unclosed bold") # => "**unclosed bold**"
|
|
13
|
+
MaquinaRemend.call("`code") # => "`code`"
|
|
14
|
+
MaquinaRemend.call("[label](https://exa") # => "[label](#)"
|
|
15
|
+
MaquinaRemend.call("<thinking>\nstill reasoning")
|
|
16
|
+
# => "<thinking>\nstill reasoning</thinking>"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It is a pure function. It is idempotent. A document that is already well formed
|
|
20
|
+
comes back byte for byte. **Zero runtime dependencies, and no Rails** — it runs
|
|
21
|
+
in a plain Ruby process with nothing else loaded.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
bundle add maquina_remend
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
or, in a Gemfile:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem "maquina_remend"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Ruby >= 3.1.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require "maquina_remend"
|
|
41
|
+
|
|
42
|
+
MaquinaRemend.call(markdown, **options) # => String
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Call it on every frame of the stream, on the whole buffer so far, and render
|
|
46
|
+
the result. Nothing is remembered between calls.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
buffer = +""
|
|
50
|
+
|
|
51
|
+
stream.each do |chunk|
|
|
52
|
+
buffer << chunk
|
|
53
|
+
render MaquinaRemend.call(buffer)
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`nil` and `""` come back exactly as they went in. An unrecognised option raises
|
|
58
|
+
`MaquinaRemend::UnknownOption` rather than being ignored, so a typo in a host's
|
|
59
|
+
configuration surfaces at once.
|
|
60
|
+
|
|
61
|
+
## Options
|
|
62
|
+
|
|
63
|
+
Every repair is individually disableable, and one is off by default.
|
|
64
|
+
|
|
65
|
+
| Option | Default | Broken input | Repaired to |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| `bold:` | `true` | `**unclosed bold` | `**unclosed bold**` |
|
|
68
|
+
| `italic:` | `true` | `*unclosed` / `_unclosed` | `*unclosed*` / `_unclosed_` |
|
|
69
|
+
| `bold_italic:` | `true` | `***both` | `***both***` |
|
|
70
|
+
| `inline_code:` | `true` | `` `code `` | `` `code` `` |
|
|
71
|
+
| `strikethrough:` | `true` | `~~struck` | `~~struck~~` |
|
|
72
|
+
| `links:` | `true` | `[label` | `[label]()` |
|
|
73
|
+
| `images:` | `true` | `![alt` | `![alt]()` |
|
|
74
|
+
| `block_math:` | `true` | `$$x = 1` | `$$x = 1$$` |
|
|
75
|
+
| `inline_math:` | **`false`** | `$x = 1` | `$x = 1$` |
|
|
76
|
+
| `setext_headings:` | `true` | `Title\n=` | `Title\n=====` |
|
|
77
|
+
| `comparison_operators:` | `true` | `- a > b` | `- a \> b` |
|
|
78
|
+
| `html_tags:` | `true` | `text <div cla` | `text` |
|
|
79
|
+
| `single_tilde:` | `true` | `20~25°C` | `20\~25°C` |
|
|
80
|
+
| `dangling_escape:` | `true` | `media ecuación \` | `media ecuación` and a space |
|
|
81
|
+
| `app_tags:` | five names | `<thinking>\nstill reasoning` | `<thinking>\nstill reasoning</thinking>` |
|
|
82
|
+
| `link_mode:` | `:protocol` | `[label](http` | `[label](#)` |
|
|
83
|
+
| `handlers:` | `[]` | — | your own handlers, run last |
|
|
84
|
+
|
|
85
|
+
Pass `false` to switch a repair off:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
MaquinaRemend.call("**unclosed bold", bold: false) # => "**unclosed bold"
|
|
89
|
+
MaquinaRemend.call("![alt", images: false) # => "![alt"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**`inline_math:` is off because a bare dollar sign is currency far more often
|
|
93
|
+
than it is mathematics.** Turn it on only if you know your corpus.
|
|
94
|
+
|
|
95
|
+
**`app_tags:` is a list, not a boolean.** It names the tags a model emits to
|
|
96
|
+
carry application meaning — `thinking`, `answer`, `tool_call`, `citation`,
|
|
97
|
+
`scratchpad` by default — and closes the ones the model has left open. Pass
|
|
98
|
+
your own names to match the host's tag registry, or `false` to switch it off:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
MaquinaRemend.call("<plan>\nstep one", app_tags: %w[plan])
|
|
102
|
+
# => "<plan>\nstep one</plan>"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Every repair, with the input it fires on and the input it refuses to touch, is
|
|
106
|
+
in **[docs/repairs.md](docs/repairs.md)**. The reasoning behind the `app_tags:`
|
|
107
|
+
list is under [Application tags](docs/repairs.md#application-tags).
|
|
108
|
+
|
|
109
|
+
## What it will not do
|
|
110
|
+
|
|
111
|
+
Guards matter more than completions. A false repair corrupts a message that was
|
|
112
|
+
never broken, and there is no frame later in the stream that undoes it.
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
MaquinaRemend.call("```\n**not bold") # => "```\n**not bold"
|
|
116
|
+
MaquinaRemend.call("`a * b`") # => "`a * b`"
|
|
117
|
+
MaquinaRemend.call("$$a_1 + b_2$$") # => "$$a_1 + b_2$$"
|
|
118
|
+
MaquinaRemend.call("some_var_name") # => "some_var_name"
|
|
119
|
+
MaquinaRemend.call("costs $5 and $10") # => "costs $5 and $10"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Nothing is completed inside a fenced code block, inside an inline code span, or
|
|
123
|
+
inside math, because in those places broken-looking markup is the content. The
|
|
124
|
+
full list is in [docs/repairs.md](docs/repairs.md#guards).
|
|
125
|
+
|
|
126
|
+
## Documentation
|
|
127
|
+
|
|
128
|
+
* **[docs/repairs.md](docs/repairs.md)** — every repair, one section each, with
|
|
129
|
+
the input it fires on and the guards that hold it back.
|
|
130
|
+
* **[docs/handlers.md](docs/handlers.md)** — writing a handler of your own: the
|
|
131
|
+
contract, the `Context` object, ordering, and a worked example.
|
|
132
|
+
* **[docs/streaming.md](docs/streaming.md)** — using it in a stream, what
|
|
133
|
+
idempotence and prefix safety buy a caller, and what the gem deliberately
|
|
134
|
+
does not do.
|
|
135
|
+
|
|
136
|
+
API documentation for every class is on
|
|
137
|
+
[rubydoc.info](https://rubydoc.info/gems/maquina_remend).
|
|
138
|
+
|
|
139
|
+
## Guarantees
|
|
140
|
+
|
|
141
|
+
Three properties, asserted over a corpus of whole documents at every truncation
|
|
142
|
+
point rather than over hand-picked inputs:
|
|
143
|
+
|
|
144
|
+
1. **Idempotence** — `call(call(x)) == call(x)`.
|
|
145
|
+
2. **No-op on well-formed input** — a complete document comes back
|
|
146
|
+
byte-identical.
|
|
147
|
+
3. **Prefix safety** — every truncation point of a document is repairable
|
|
148
|
+
without raising.
|
|
149
|
+
|
|
150
|
+
A fourth is measured rather than proved: an 8KB buffer is repaired in under a
|
|
151
|
+
millisecond.
|
|
152
|
+
|
|
153
|
+
Run them with `bundle exec rake test`. The prefix-safety property also accepts a
|
|
154
|
+
local corpus of real transcripts:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
MAQUINA_REMEND_CORPUS=~/some/transcripts bundle exec rake test
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
That corpus is deliberately not committed: a transcript contains whatever the
|
|
161
|
+
session contained.
|
|
162
|
+
|
|
163
|
+
## maquina
|
|
164
|
+
|
|
165
|
+
Part of [maquina](https://maquina.app) — open source for Ruby and Ruby AI.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT, © Mario Alberto Chávez. See [LICENSE.txt](LICENSE.txt).
|
data/docs/handlers.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Custom handlers
|
|
2
|
+
|
|
3
|
+
The built-in repairs are listed in [repairs.md](repairs.md). When a host needs
|
|
4
|
+
one of its own — a template placeholder, a shortcode, a marker the model emits
|
|
5
|
+
that no markdown parser knows about — it passes a handler.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
MaquinaRemend.call(markdown, handlers: [MyHandler.new])
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## The contract
|
|
12
|
+
|
|
13
|
+
A handler is **any object that responds to `#call(text, context)` and returns a
|
|
14
|
+
String.** There is no class to inherit from and no registration step.
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
class MyHandler
|
|
18
|
+
def call(text, context) # context: MaquinaRemend::Context
|
|
19
|
+
text
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
* **`text`** is the buffer as the built-ins left it — already repaired.
|
|
25
|
+
* **`context`** is a `MaquinaRemend::Context` describing where the tail sits.
|
|
26
|
+
* **Return a String.** Returning the argument unchanged is the normal outcome;
|
|
27
|
+
most passes over most buffers repair nothing.
|
|
28
|
+
* **Handlers run in the order given**, each seeing the previous one's output.
|
|
29
|
+
* **Stay idempotent, and stay byte-identical on well-formed input.** Those two
|
|
30
|
+
properties belong to the library, not just to the built-ins. A handler that
|
|
31
|
+
appends unconditionally breaks both: the text it appended dangles on the next
|
|
32
|
+
pass and gets appended to again.
|
|
33
|
+
|
|
34
|
+
## The `Context` object
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
context = MaquinaRemend.context("~~~ruby\ndef call")
|
|
38
|
+
context.in_code_fence? # => true
|
|
39
|
+
context.open_fence_info # => "ruby"
|
|
40
|
+
context.in_inline_code? # => false
|
|
41
|
+
context.in_math? # => false
|
|
42
|
+
|
|
43
|
+
MaquinaRemend.context("a `code").in_inline_code? # => true
|
|
44
|
+
MaquinaRemend.context("$$x = 1").in_math? # => true
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Three predicates and two readers, and nothing else:
|
|
48
|
+
|
|
49
|
+
| Member | Answers |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `#in_code_fence?` | is the tail inside an unclosed fenced code block? |
|
|
52
|
+
| `#in_inline_code?` | is an unclosed backtick span open in this paragraph? |
|
|
53
|
+
| `#in_math?` | is an unclosed `$$` block-math span open? |
|
|
54
|
+
| `#open_fence_info` | the open fence's info string, or `nil` |
|
|
55
|
+
| `#options` | the resolved option hash the pipeline was built with |
|
|
56
|
+
|
|
57
|
+
The three predicates are the three places where broken-looking markup is the
|
|
58
|
+
content rather than a defect. A repair that needs more state than they expose
|
|
59
|
+
means the scanner is missing a state, not that `Context` should grow.
|
|
60
|
+
|
|
61
|
+
`#open_fence_info` is `nil` when no fence is open, and also when the open fence
|
|
62
|
+
carried no info string:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
MaquinaRemend.context("~~~ruby\ndef call").open_fence_info # => "ruby"
|
|
66
|
+
MaquinaRemend.context("~~~\nplain").open_fence_info # => nil
|
|
67
|
+
MaquinaRemend.context("plain text").open_fence_info # => nil
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`MaquinaRemend.context` is public for callers that want the reading without the
|
|
71
|
+
repair — a renderer deciding whether to run a syntax highlighter over a block
|
|
72
|
+
whose closing fence has not streamed in yet:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
context = MaquinaRemend.context(buffer)
|
|
76
|
+
highlight(buffer, language: context.open_fence_info) if context.in_code_fence?
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Its `#options` is empty, because it did no repair and therefore had no options.
|
|
80
|
+
|
|
81
|
+
## Ordering
|
|
82
|
+
|
|
83
|
+
**Custom handlers run last, after every built-in.** Two consequences.
|
|
84
|
+
|
|
85
|
+
First, the text a handler receives already carries the built-ins' completions.
|
|
86
|
+
An `upcase` handler sees the closer the emphasis repair appended:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
shouty = Object.new
|
|
90
|
+
def shouty.call(text, _context) = text.upcase
|
|
91
|
+
|
|
92
|
+
MaquinaRemend.call("**bold", handlers: [shouty]) # => "**BOLD**"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If your handler anchors on the end of the buffer, remember that the end of the
|
|
96
|
+
buffer may no longer be where the model stopped typing.
|
|
97
|
+
|
|
98
|
+
Second, **a custom handler still runs inside a fenced code block.** The
|
|
99
|
+
built-ins all return early there; a custom handler does not, because it is given
|
|
100
|
+
the context and trusted to decide — which it could not do if it never ran. Ask
|
|
101
|
+
for yourself:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
return text if context.in_code_fence?
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## A worked example
|
|
108
|
+
|
|
109
|
+
A host renders emoji shortcodes, `:smile:`. Mid-stream the model has typed
|
|
110
|
+
`:smi`, and the reader sees a stray colon and three letters that will change
|
|
111
|
+
under them a frame later. Drop the partial shortcode until it is whole.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
class PartialShortcode
|
|
115
|
+
TRAILING = /(?<=\A|\s):[a-z][a-z0-9_+-]*\z/
|
|
116
|
+
|
|
117
|
+
def call(text, context)
|
|
118
|
+
return text if context.in_code_fence?
|
|
119
|
+
|
|
120
|
+
text.sub(TRAILING, "").rstrip
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
handler = PartialShortcode.new
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
It fires on a shortcode that is still arriving:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
MaquinaRemend.call("Nice work :smi", handlers: [handler])
|
|
131
|
+
# => "Nice work"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
It leaves a finished one alone — the pattern cannot match a trailing `:`, so a
|
|
135
|
+
complete document passes through untouched:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
MaquinaRemend.call("Nice work :smile:", handlers: [handler])
|
|
139
|
+
# => "Nice work :smile:"
|
|
140
|
+
MaquinaRemend.call("Nice work", handlers: [handler])
|
|
141
|
+
# => "Nice work"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
It leaves a fence alone, because it asked:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
MaquinaRemend.call("```\nNice work :smi", handlers: [handler])
|
|
148
|
+
# => "```\nNice work :smi"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The lookbehind is what keeps it off a colon that was never a shortcode:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
MaquinaRemend.call("ratio 3:2", handlers: [handler])
|
|
155
|
+
# => "ratio 3:2"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
And it is idempotent: running it over its own output changes nothing, in every
|
|
159
|
+
case above.
|
|
160
|
+
|
|
161
|
+
The one case it does not catch is the ordering rule from the previous section.
|
|
162
|
+
The emphasis repair has already appended `**`, so the shortcode is no longer at
|
|
163
|
+
the end of the buffer and the anchor misses:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
MaquinaRemend.call("**Nice work :smi", handlers: [handler])
|
|
167
|
+
# => "**Nice work :smi**"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
That is the trade the ordering makes, and it is the right way round: a custom
|
|
171
|
+
handler that ran *before* the built-ins would be repairing a buffer the
|
|
172
|
+
built-ins were about to rewrite underneath it. A handler that must see the raw
|
|
173
|
+
tail should match on the paragraph rather than on `\z`.
|
|
174
|
+
|
|
175
|
+
## Reusing a validated pipeline
|
|
176
|
+
|
|
177
|
+
`MaquinaRemend.call` builds a `Pipeline` per call and throws it away. Options
|
|
178
|
+
are validated in the constructor, so building one yourself is worth it when the
|
|
179
|
+
same options are used for every frame of a long stream:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
pipeline = MaquinaRemend::Pipeline.new(inline_math: true)
|
|
183
|
+
pipeline.call("$x = 1") # => "$x = 1$"
|
|
184
|
+
|
|
185
|
+
deltas.map { |buffer| pipeline.call(buffer) }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
It holds no state between buffers. Validation happening in the constructor also
|
|
189
|
+
means a bad option is a startup error for the host rather than a surprise
|
|
190
|
+
halfway through a stream:
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
MaquinaRemend::Pipeline.new(bolder: true)
|
|
194
|
+
# MaquinaRemend::UnknownOption: unknown option(s): bolder
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Where a custom handler runs in the order
|
|
198
|
+
|
|
199
|
+
The built-ins run in a fixed order, and then yours:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
MaquinaRemend::Pipeline::BUILT_INS.map(&:name)
|
|
203
|
+
# => ["MaquinaRemend::Handlers::DanglingEscape",
|
|
204
|
+
# "MaquinaRemend::Handlers::HtmlTags",
|
|
205
|
+
# "MaquinaRemend::Handlers::InlineCode",
|
|
206
|
+
# "MaquinaRemend::Handlers::Links",
|
|
207
|
+
# "MaquinaRemend::Handlers::Math",
|
|
208
|
+
# "MaquinaRemend::Handlers::Emphasis",
|
|
209
|
+
# "MaquinaRemend::Handlers::Strikethrough",
|
|
210
|
+
# "MaquinaRemend::Handlers::SetextHeading",
|
|
211
|
+
# "MaquinaRemend::Handlers::ComparisonOperators",
|
|
212
|
+
# "MaquinaRemend::Handlers::SingleTilde",
|
|
213
|
+
# "MaquinaRemend::Handlers::AppTags"]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The order is load-bearing. A dangling backslash is dropped first, because it
|
|
217
|
+
would escape whatever any later handler appends. Links run before emphasis,
|
|
218
|
+
because an emphasis closer appended into a half-typed `[label(` lands inside
|
|
219
|
+
the destination. Application tags run last, because their closer has to sit
|
|
220
|
+
outside every other completion.
|
|
221
|
+
|
|
222
|
+
The buffer is rescanned after any handler that changed it, so no handler reads
|
|
223
|
+
a view of the text that an earlier one has already invalidated.
|