inkmark 0.2.2 → 0.3.1
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/CHANGELOG.md +14 -0
- data/Cargo.lock +4 -3
- data/README.md +20 -0
- data/ext/inkmark/Cargo.toml +2 -2
- data/ext/inkmark/src/chunks_by_heading.rs +91 -31
- data/ext/inkmark/src/chunks_by_size.rs +33 -18
- data/ext/inkmark/src/document.rs +22 -20
- data/ext/inkmark/src/image.rs +57 -30
- data/ext/inkmark/src/lib.rs +1 -0
- data/ext/inkmark/src/link.rs +10 -9
- data/ext/inkmark/src/markdown.rs +436 -0
- data/ext/inkmark/src/scheme_filter.rs +31 -6
- data/ext/inkmark/src/truncate.rs +40 -23
- data/lib/inkmark/version.rb +1 -1
- metadata +2 -1
|
@@ -35,8 +35,10 @@ pub struct SchemeFilter<'e, 's, I: Iterator<Item = Event<'e>>> {
|
|
|
35
35
|
// disallows nested links so this is usually 1, but the counter keeps
|
|
36
36
|
// us correct on malformed / extension-emitted streams.
|
|
37
37
|
skipping_link_depth: usize,
|
|
38
|
-
// State for in-progress image drop
|
|
39
|
-
|
|
38
|
+
// State for in-progress image drop, counted like links: an image can
|
|
39
|
+
// nest in another image's alt text (`](y)`). 0 = not
|
|
40
|
+
// skipping.
|
|
41
|
+
skipping_image_depth: usize,
|
|
40
42
|
image_alt: String,
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -51,7 +53,7 @@ impl<'e, 's, I: Iterator<Item = Event<'e>>> SchemeFilter<'e, 's, I> {
|
|
|
51
53
|
link_allowed,
|
|
52
54
|
image_allowed,
|
|
53
55
|
skipping_link_depth: 0,
|
|
54
|
-
|
|
56
|
+
skipping_image_depth: 0,
|
|
55
57
|
image_alt: String::new(),
|
|
56
58
|
}
|
|
57
59
|
}
|
|
@@ -63,10 +65,17 @@ impl<'e, 's, I: Iterator<Item = Event<'e>>> Iterator for SchemeFilter<'e, 's, I>
|
|
|
63
65
|
fn next(&mut self) -> Option<Event<'e>> {
|
|
64
66
|
loop {
|
|
65
67
|
// Image skipping mode
|
|
66
|
-
if self.
|
|
68
|
+
if self.skipping_image_depth > 0 {
|
|
67
69
|
match self.inner.next()? {
|
|
70
|
+
Event::Start(Tag::Image { .. }) => {
|
|
71
|
+
self.skipping_image_depth += 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
68
74
|
Event::End(TagEnd::Image) => {
|
|
69
|
-
self.
|
|
75
|
+
self.skipping_image_depth -= 1;
|
|
76
|
+
if self.skipping_image_depth > 0 {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
70
79
|
if !self.image_alt.is_empty() {
|
|
71
80
|
let alt = std::mem::take(&mut self.image_alt);
|
|
72
81
|
return Some(Event::Text(CowStr::Boxed(alt.into_boxed_str())));
|
|
@@ -117,7 +126,7 @@ impl<'e, 's, I: Iterator<Item = Event<'e>>> Iterator for SchemeFilter<'e, 's, I>
|
|
|
117
126
|
.image_allowed
|
|
118
127
|
.is_some_and(|s| !is_scheme_allowed(dest_url, s)) =>
|
|
119
128
|
{
|
|
120
|
-
self.
|
|
129
|
+
self.skipping_image_depth = 1;
|
|
121
130
|
self.image_alt.clear();
|
|
122
131
|
continue;
|
|
123
132
|
}
|
|
@@ -162,6 +171,22 @@ mod tests {
|
|
|
162
171
|
assert!(matches!(out[0], Event::Start(Tag::Link { .. })));
|
|
163
172
|
}
|
|
164
173
|
|
|
174
|
+
#[test]
|
|
175
|
+
fn drops_blocked_image_with_an_image_nested_in_its_alt_text() {
|
|
176
|
+
let source = "](http://x.com/2)";
|
|
177
|
+
let events: Vec<Event> = pulldown_cmark::Parser::new(source).collect();
|
|
178
|
+
let allowed = schemes(&["https"]);
|
|
179
|
+
let out = run(events, None, Some(&allowed));
|
|
180
|
+
assert!(
|
|
181
|
+
!out.iter().any(|e| matches!(
|
|
182
|
+
e,
|
|
183
|
+
Event::Start(Tag::Image { .. }) | Event::End(TagEnd::Image)
|
|
184
|
+
)),
|
|
185
|
+
"stray image event: {out:?}"
|
|
186
|
+
);
|
|
187
|
+
assert!(out.contains(&Event::Text(CowStr::Borrowed("foo bar"))));
|
|
188
|
+
}
|
|
189
|
+
|
|
165
190
|
#[test]
|
|
166
191
|
fn drops_blocked_link_keeps_text() {
|
|
167
192
|
let events = vec![
|
data/ext/inkmark/src/truncate.rs
CHANGED
|
@@ -11,6 +11,7 @@ use pulldown_cmark::Event;
|
|
|
11
11
|
use unicode_segmentation::UnicodeSegmentation;
|
|
12
12
|
|
|
13
13
|
use crate::document::{apply_filters, content_events};
|
|
14
|
+
use crate::markdown::{self, SerializeError};
|
|
14
15
|
use crate::options::{build_options, Flags};
|
|
15
16
|
|
|
16
17
|
/// What kind of boundary to cut at.
|
|
@@ -43,10 +44,14 @@ pub fn truncate_source(
|
|
|
43
44
|
cm_opts: pulldown_cmark::Options,
|
|
44
45
|
flags: &Flags,
|
|
45
46
|
params: &TruncateParams,
|
|
46
|
-
) -> String {
|
|
47
|
+
) -> Result<String, SerializeError> {
|
|
47
48
|
let events: Vec<Event> = content_events(source, cm_opts).collect();
|
|
48
49
|
let events = apply_filters(events, flags);
|
|
49
|
-
truncate_events(
|
|
50
|
+
truncate_events(
|
|
51
|
+
&events,
|
|
52
|
+
params,
|
|
53
|
+
markdown::serializer_options(cm_opts, flags),
|
|
54
|
+
)
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
/// Ruby-facing entry point. Expects `params_hash` to contain
|
|
@@ -61,7 +66,7 @@ pub fn native_truncate_markdown(
|
|
|
61
66
|
) -> Result<String, Error> {
|
|
62
67
|
let (cm_opts, flags) = build_options(ruby, opts_hash)?;
|
|
63
68
|
let params = parse_params(ruby, params_hash)?;
|
|
64
|
-
|
|
69
|
+
truncate_source(&source, cm_opts, &flags, ¶ms).map_err(|e| markdown::to_ruby_error(ruby, e))
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
pub fn parse_params(ruby: &Ruby, hash: RHash) -> Result<TruncateParams, Error> {
|
|
@@ -85,14 +90,22 @@ pub fn parse_params(ruby: &Ruby, hash: RHash) -> Result<TruncateParams, Error> {
|
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
/// Core truncation over a filter-applied event slice.
|
|
88
|
-
pub fn truncate_events(
|
|
93
|
+
pub fn truncate_events(
|
|
94
|
+
events: &[Event<'_>],
|
|
95
|
+
params: &TruncateParams,
|
|
96
|
+
md_opts: pulldown_cmark::Options,
|
|
97
|
+
) -> Result<String, SerializeError> {
|
|
89
98
|
match params.at {
|
|
90
|
-
TruncateAt::Block => truncate_at_block(events, params),
|
|
91
|
-
TruncateAt::Word => truncate_at_word(events, params),
|
|
99
|
+
TruncateAt::Block => truncate_at_block(events, params, md_opts),
|
|
100
|
+
TruncateAt::Word => truncate_at_word(events, params, md_opts),
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
fn truncate_at_block(
|
|
104
|
+
fn truncate_at_block(
|
|
105
|
+
events: &[Event<'_>],
|
|
106
|
+
params: &TruncateParams,
|
|
107
|
+
md_opts: pulldown_cmark::Options,
|
|
108
|
+
) -> Result<String, SerializeError> {
|
|
96
109
|
let marker_chars = marker_chars(¶ms.marker);
|
|
97
110
|
let marker_words = marker_words(¶ms.marker);
|
|
98
111
|
let char_budget = params.chars.map(|n| n.saturating_sub(marker_chars));
|
|
@@ -104,8 +117,14 @@ fn truncate_at_block(events: &[Event<'_>], params: &TruncateParams) -> String {
|
|
|
104
117
|
let mut any_dropped = false;
|
|
105
118
|
|
|
106
119
|
for (start, end) in top_level_blocks(events) {
|
|
107
|
-
let block =
|
|
108
|
-
|
|
120
|
+
let block = markdown::render_block(&events[start..=end], md_opts)?;
|
|
121
|
+
// The blank line rejoining blocks counts toward the char budget.
|
|
122
|
+
let separator = if kept.is_empty() {
|
|
123
|
+
""
|
|
124
|
+
} else {
|
|
125
|
+
markdown::BLOCK_SEPARATOR
|
|
126
|
+
};
|
|
127
|
+
let block_chars = separator.len() + block.trim_end().chars().count();
|
|
109
128
|
let block_words = block.unicode_words().count();
|
|
110
129
|
|
|
111
130
|
let would_exceed_chars = char_budget
|
|
@@ -120,6 +139,7 @@ fn truncate_at_block(events: &[Event<'_>], params: &TruncateParams) -> String {
|
|
|
120
139
|
break;
|
|
121
140
|
}
|
|
122
141
|
|
|
142
|
+
kept.push_str(separator);
|
|
123
143
|
kept.push_str(&block);
|
|
124
144
|
used_chars += block_chars;
|
|
125
145
|
used_words += block_words;
|
|
@@ -127,12 +147,12 @@ fn truncate_at_block(events: &[Event<'_>], params: &TruncateParams) -> String {
|
|
|
127
147
|
|
|
128
148
|
// If nothing was dropped and source fit, return unchanged (no marker).
|
|
129
149
|
if !any_dropped {
|
|
130
|
-
return kept;
|
|
150
|
+
return Ok(kept);
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
// First block alone exceeded the budget; honest empty return
|
|
134
154
|
if kept.is_empty() {
|
|
135
|
-
return String::new();
|
|
155
|
+
return Ok(String::new());
|
|
136
156
|
}
|
|
137
157
|
|
|
138
158
|
// Trim trailing whitespace before appending the marker so the
|
|
@@ -145,7 +165,7 @@ fn truncate_at_block(events: &[Event<'_>], params: &TruncateParams) -> String {
|
|
|
145
165
|
kept.push_str(marker);
|
|
146
166
|
}
|
|
147
167
|
kept.push('\n');
|
|
148
|
-
kept
|
|
168
|
+
Ok(kept)
|
|
149
169
|
}
|
|
150
170
|
|
|
151
171
|
/// Return (start, end) event-index pairs for top-level blocks.
|
|
@@ -185,15 +205,12 @@ fn top_level_blocks(events: &[Event<'_>]) -> Vec<(usize, usize)> {
|
|
|
185
205
|
blocks
|
|
186
206
|
}
|
|
187
207
|
|
|
188
|
-
fn
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
fn truncate_at_word(events: &[Event<'_>], params: &TruncateParams) -> String {
|
|
196
|
-
let rendered = render_markdown(events);
|
|
208
|
+
fn truncate_at_word(
|
|
209
|
+
events: &[Event<'_>],
|
|
210
|
+
params: &TruncateParams,
|
|
211
|
+
md_opts: pulldown_cmark::Options,
|
|
212
|
+
) -> Result<String, SerializeError> {
|
|
213
|
+
let rendered = markdown::render(events, md_opts)?;
|
|
197
214
|
let marker_chars = marker_chars(¶ms.marker);
|
|
198
215
|
let marker_words = marker_words(¶ms.marker);
|
|
199
216
|
|
|
@@ -211,7 +228,7 @@ fn truncate_at_word(events: &[Event<'_>], params: &TruncateParams) -> String {
|
|
|
211
228
|
|
|
212
229
|
// Fits under both budgets: return unchanged, no marker.
|
|
213
230
|
if chars_ok && words_ok {
|
|
214
|
-
return rendered;
|
|
231
|
+
return Ok(rendered);
|
|
215
232
|
}
|
|
216
233
|
|
|
217
234
|
let char_budget = params.chars.map(|n| n.saturating_sub(marker_chars));
|
|
@@ -252,7 +269,7 @@ fn truncate_at_word(events: &[Event<'_>], params: &TruncateParams) -> String {
|
|
|
252
269
|
if let Some(marker) = ¶ms.marker {
|
|
253
270
|
out.push_str(marker);
|
|
254
271
|
}
|
|
255
|
-
out
|
|
272
|
+
Ok(out)
|
|
256
273
|
}
|
|
257
274
|
|
|
258
275
|
fn marker_chars(marker: &Option<String>) -> usize {
|
data/lib/inkmark/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: inkmark
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Markin
|
|
@@ -164,6 +164,7 @@ files:
|
|
|
164
164
|
- ext/inkmark/src/image.rs
|
|
165
165
|
- ext/inkmark/src/lib.rs
|
|
166
166
|
- ext/inkmark/src/link.rs
|
|
167
|
+
- ext/inkmark/src/markdown.rs
|
|
167
168
|
- ext/inkmark/src/options.rs
|
|
168
169
|
- ext/inkmark/src/plain_text.rs
|
|
169
170
|
- ext/inkmark/src/scheme_filter.rs
|