markbridge 0.4.3 → 0.5.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 +4 -4
- data/lib/markbridge/ast/image.rb +9 -1
- data/lib/markbridge/ast/url.rb +29 -4
- data/lib/markbridge/errors.rb +9 -0
- data/lib/markbridge/parsers/bbcode/handlers/image_handler.rb +2 -1
- data/lib/markbridge/parsers/bbcode/handlers/url_handler.rb +26 -2
- data/lib/markbridge/parsers/html/handlers/image_handler.rb +4 -1
- data/lib/markbridge/parsers/text_formatter/handlers/attachment_handler.rb +3 -2
- data/lib/markbridge/parsers/text_formatter/handlers/image_handler.rb +4 -2
- data/lib/markbridge/parsers/text_formatter/parser.rb +107 -14
- data/lib/markbridge/renderers/discourse/html_block.rb +50 -7
- data/lib/markbridge/renderers/discourse/inline/joins.rb +382 -0
- data/lib/markbridge/renderers/discourse/inline/serializer.rb +738 -0
- data/lib/markbridge/renderers/discourse/inline/tokens.rb +182 -0
- data/lib/markbridge/renderers/discourse/link_destination.rb +43 -0
- data/lib/markbridge/renderers/discourse/markdown_escaper.rb +105 -45
- data/lib/markbridge/renderers/discourse/paragraphs.rb +88 -0
- data/lib/markbridge/renderers/discourse/postprocessor.rb +183 -7
- data/lib/markbridge/renderers/discourse/render_context.rb +38 -12
- data/lib/markbridge/renderers/discourse/renderer.rb +336 -92
- data/lib/markbridge/renderers/discourse/rendering_interface.rb +123 -44
- data/lib/markbridge/renderers/discourse/tag.rb +53 -9
- data/lib/markbridge/renderers/discourse/tags/bold_tag.rb +4 -6
- data/lib/markbridge/renderers/discourse/tags/code_tag.rb +48 -10
- data/lib/markbridge/renderers/discourse/tags/color_tag.rb +73 -13
- data/lib/markbridge/renderers/discourse/tags/details_tag.rb +62 -15
- data/lib/markbridge/renderers/discourse/tags/email_tag.rb +8 -9
- data/lib/markbridge/renderers/discourse/tags/event_tag.rb +31 -9
- data/lib/markbridge/renderers/discourse/tags/heading_tag.rb +20 -1
- data/lib/markbridge/renderers/discourse/tags/horizontal_rule_tag.rb +12 -1
- data/lib/markbridge/renderers/discourse/tags/image_tag.rb +13 -27
- data/lib/markbridge/renderers/discourse/tags/italic_tag.rb +4 -6
- data/lib/markbridge/renderers/discourse/tags/line_break_tag.rb +9 -2
- data/lib/markbridge/renderers/discourse/tags/list_item_tag.rb +75 -9
- data/lib/markbridge/renderers/discourse/tags/list_tag.rb +29 -7
- data/lib/markbridge/renderers/discourse/tags/mention_tag.rb +12 -0
- data/lib/markbridge/renderers/discourse/tags/paragraph_tag.rb +13 -6
- data/lib/markbridge/renderers/discourse/tags/poll_tag.rb +52 -9
- data/lib/markbridge/renderers/discourse/tags/quote_tag.rb +26 -1
- data/lib/markbridge/renderers/discourse/tags/size_tag.rb +78 -13
- data/lib/markbridge/renderers/discourse/tags/spoiler_tag.rb +72 -14
- data/lib/markbridge/renderers/discourse/tags/strikethrough_tag.rb +5 -6
- data/lib/markbridge/renderers/discourse/tags/subscript_tag.rb +11 -5
- data/lib/markbridge/renderers/discourse/tags/superscript_tag.rb +11 -5
- data/lib/markbridge/renderers/discourse/tags/table_tag.rb +48 -28
- data/lib/markbridge/renderers/discourse/tags/underline_tag.rb +3 -10
- data/lib/markbridge/renderers/discourse/tags/upload_tag.rb +90 -43
- data/lib/markbridge/renderers/discourse/tags/url_tag.rb +41 -31
- data/lib/markbridge/renderers/discourse.rb +6 -0
- data/lib/markbridge/rspec.rb +4 -2
- data/lib/markbridge/version.rb +1 -1
- data/lib/markbridge.rb +90 -1
- metadata +7 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markbridge
|
|
4
|
+
module Renderers
|
|
5
|
+
module Discourse
|
|
6
|
+
module Inline
|
|
7
|
+
# What has to happen where two pieces of inline Markdown meet. The
|
|
8
|
+
# rules are a table read from the class of the byte in front of
|
|
9
|
+
# the join and the byte after it, so the common case, nothing to
|
|
10
|
+
# do, costs four byte reads and no method call.
|
|
11
|
+
#
|
|
12
|
+
# A run of emphasis delimiters at the start of the piece cannot
|
|
13
|
+
# open when a word character stands in front of it and
|
|
14
|
+
# punctuation follows it: `item*\#*` stays literal text
|
|
15
|
+
# (CommonMark 6.2). The mirror image holds for a run at the end
|
|
16
|
+
# of the buffer. An empty HTML comment between the two sides is
|
|
17
|
+
# punctuation, so the run can open or close again. Two runs of
|
|
18
|
+
# the same delimiter would merge into one longer run, the
|
|
19
|
+
# comment keeps them apart. A `!` right in front of a link makes
|
|
20
|
+
# it an image, a backslash keeps it a `!`.
|
|
21
|
+
#
|
|
22
|
+
# Two more joins are what the escaper would escape inside one
|
|
23
|
+
# text, but cannot see across two pieces: a `<` at the end of the
|
|
24
|
+
# buffer with a `!`, `?`, `/` or a letter after it would start raw
|
|
25
|
+
# HTML (a declaration like `<!y` eats the text up to the next
|
|
26
|
+
# `>`), so the `<` gets a backslash; two `-` that meet would be a
|
|
27
|
+
# dash to Discourse's typographer, so the second gets one.
|
|
28
|
+
module Joins
|
|
29
|
+
# Inserted between two pieces. Invisible when cooked, and
|
|
30
|
+
# punctuation to the flanking rules.
|
|
31
|
+
BOUNDARY_COMMENT = "<!---->"
|
|
32
|
+
|
|
33
|
+
# A punctuation character or a symbol right at the search start,
|
|
34
|
+
# which is what CommonMark's flanking rules count as punctuation
|
|
35
|
+
# (ASCII and Unicode alike), as markdown-it does. For a character
|
|
36
|
+
# outside ASCII only; an ASCII byte is answered from the class
|
|
37
|
+
# table without a regex.
|
|
38
|
+
PUNCTUATION_AT = /\A[\p{P}\p{S}]/
|
|
39
|
+
# Whitespace outside ASCII, matched on the one character.
|
|
40
|
+
SPACE_AT = /\A[[:space:]]/
|
|
41
|
+
BACKSLASH = 92
|
|
42
|
+
SPACE = 32
|
|
43
|
+
TAB = 9
|
|
44
|
+
CR = 13
|
|
45
|
+
|
|
46
|
+
# What a byte is to the join rules. WORD: ASCII letters and
|
|
47
|
+
# digits, and every non-ASCII byte. On the side of the join away
|
|
48
|
+
# from a delimiter run, Unicode punctuation then goes through the
|
|
49
|
+
# check like a letter, which at worst costs a comment that
|
|
50
|
+
# changes nothing; on the side of the run the check reads the
|
|
51
|
+
# whole character (.punctuation_at?). PUNCTUATION: ASCII
|
|
52
|
+
# punctuation as CommonMark defines it, the backslash included,
|
|
53
|
+
# which is what makes an escaped character at the edge of an
|
|
54
|
+
# emphasis count as punctuation. The five bytes the rules name
|
|
55
|
+
# get a class of their own, so a class tells them apart without
|
|
56
|
+
# a second comparison; they stay punctuation, which is why they
|
|
57
|
+
# sit above PUNCTUATION. AT is the `@` of a mention: after a word
|
|
58
|
+
# character, or punctuation that an email address may hold, it
|
|
59
|
+
# would read as an email address with the text after it. LT and
|
|
60
|
+
# DASH are the `<` and the `-` that the escaper would escape
|
|
61
|
+
# inside one text, see the module comment.
|
|
62
|
+
OTHER = 0
|
|
63
|
+
WORD = 1
|
|
64
|
+
PUNCTUATION = 2
|
|
65
|
+
BANG = 3
|
|
66
|
+
BRACKET = 4
|
|
67
|
+
BACKTICK = 5
|
|
68
|
+
STAR = 6
|
|
69
|
+
UNDERSCORE = 7
|
|
70
|
+
TILDE = 8
|
|
71
|
+
AT = 9
|
|
72
|
+
LT = 10
|
|
73
|
+
DASH = 11
|
|
74
|
+
CLASS_COUNT = 12
|
|
75
|
+
# A 256-byte string read with getbyte, so classifying a byte is
|
|
76
|
+
# one read instead of a chain of comparisons.
|
|
77
|
+
BYTE_CLASSES =
|
|
78
|
+
Array
|
|
79
|
+
.new(256) do |byte|
|
|
80
|
+
case byte
|
|
81
|
+
when 33
|
|
82
|
+
BANG
|
|
83
|
+
when 42
|
|
84
|
+
STAR
|
|
85
|
+
when 45
|
|
86
|
+
DASH
|
|
87
|
+
when 60
|
|
88
|
+
LT
|
|
89
|
+
when 64
|
|
90
|
+
AT
|
|
91
|
+
when 91
|
|
92
|
+
BRACKET
|
|
93
|
+
when 95
|
|
94
|
+
UNDERSCORE
|
|
95
|
+
when 96
|
|
96
|
+
BACKTICK
|
|
97
|
+
when 126
|
|
98
|
+
TILDE
|
|
99
|
+
else
|
|
100
|
+
if byte >= 128 || byte.between?(48, 57) || byte.between?(65, 90) ||
|
|
101
|
+
byte.between?(97, 122)
|
|
102
|
+
WORD
|
|
103
|
+
elsif byte.between?(33, 47) || byte.between?(58, 64) || byte.between?(91, 96) ||
|
|
104
|
+
byte.between?(123, 126)
|
|
105
|
+
PUNCTUATION
|
|
106
|
+
else
|
|
107
|
+
OTHER
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
.pack("C*")
|
|
112
|
+
.freeze
|
|
113
|
+
|
|
114
|
+
# What a join between two pieces has to do. ESCAPE_BEFORE puts a
|
|
115
|
+
# backslash in front of the last byte of the buffer (a `!` or a
|
|
116
|
+
# `<`), ESCAPE_AFTER one in front of the first byte of the piece
|
|
117
|
+
# (a `-`).
|
|
118
|
+
NOTHING = 0
|
|
119
|
+
BOUNDARY = 1
|
|
120
|
+
CHECK_AFTER = 2
|
|
121
|
+
CHECK_BEFORE = 3
|
|
122
|
+
ESCAPE_BEFORE = 4
|
|
123
|
+
ESCAPE_AFTER = 5
|
|
124
|
+
# Runs that merge into a single longer run during Markdown
|
|
125
|
+
# parsing: emphasis (* _), strikethrough (~), code spans (`).
|
|
126
|
+
MERGING_CLASSES = [STAR, UNDERSCORE, TILDE, BACKTICK].freeze
|
|
127
|
+
# Runs that open and close only where the flanking rules allow it.
|
|
128
|
+
FLANKING_CLASSES = [STAR, UNDERSCORE, TILDE].freeze
|
|
129
|
+
# The bytes after a `<` that start raw HTML (CommonMark 6.6): a
|
|
130
|
+
# tag name or `/`, a declaration `!`, a processing instruction
|
|
131
|
+
# `?`. Bytes, not classes: a digit or a non-ASCII byte is a word
|
|
132
|
+
# character too, but starts no tag.
|
|
133
|
+
HTML_START_BYTES = [33, 47, 63, *65..90, *97..122].freeze
|
|
134
|
+
|
|
135
|
+
# The action per join, in CLASS_COUNT rows of 256 bytes (2304 in
|
|
136
|
+
# all), read with JOIN_ACTIONS.getbyte((class of the byte in front
|
|
137
|
+
# << 8) + the byte after). Both sides of a join are a single byte
|
|
138
|
+
# for every rule, so the whole decision fits in the table except
|
|
139
|
+
# for the two cases that have to look past a delimiter run and the
|
|
140
|
+
# one that has to count backslashes.
|
|
141
|
+
#
|
|
142
|
+
# A `_` run never opens after, or closes in front of, a word
|
|
143
|
+
# character, so it needs no look past the run.
|
|
144
|
+
#
|
|
145
|
+
# A tilde counts as a blocking neighbor like a word character:
|
|
146
|
+
# CommonMark reads `~` as punctuation, but cmark-gfm does not
|
|
147
|
+
# treat it as one next to an emphasis delimiter, so strikethrough
|
|
148
|
+
# right next to emphasis with a punctuation edge needs the
|
|
149
|
+
# boundary as well. markdown-it would not need it; the comment
|
|
150
|
+
# changes nothing there.
|
|
151
|
+
JOIN_ACTIONS =
|
|
152
|
+
Array
|
|
153
|
+
.new(CLASS_COUNT * 256) do |index|
|
|
154
|
+
before = index >> 8
|
|
155
|
+
byte = index & 255
|
|
156
|
+
after = BYTE_CLASSES.getbyte(byte)
|
|
157
|
+
|
|
158
|
+
if before == after && MERGING_CLASSES.include?(before)
|
|
159
|
+
BOUNDARY
|
|
160
|
+
elsif before == BANG && after == BRACKET
|
|
161
|
+
ESCAPE_BEFORE
|
|
162
|
+
elsif before == LT && HTML_START_BYTES.include?(byte)
|
|
163
|
+
ESCAPE_BEFORE
|
|
164
|
+
elsif before == DASH && after == DASH
|
|
165
|
+
ESCAPE_AFTER
|
|
166
|
+
elsif FLANKING_CLASSES.include?(after) && (before == WORD || before == TILDE)
|
|
167
|
+
after == UNDERSCORE ? BOUNDARY : CHECK_AFTER
|
|
168
|
+
elsif FLANKING_CLASSES.include?(before) && (after == WORD || after == TILDE)
|
|
169
|
+
before == UNDERSCORE ? BOUNDARY : CHECK_BEFORE
|
|
170
|
+
elsif after == AT && before != OTHER
|
|
171
|
+
BOUNDARY
|
|
172
|
+
else
|
|
173
|
+
NOTHING
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
.pack("C*")
|
|
177
|
+
.freeze
|
|
178
|
+
private_constant :PUNCTUATION_AT,
|
|
179
|
+
:SPACE_AT,
|
|
180
|
+
:BACKSLASH,
|
|
181
|
+
:SPACE,
|
|
182
|
+
:TAB,
|
|
183
|
+
:CR,
|
|
184
|
+
:OTHER,
|
|
185
|
+
:WORD,
|
|
186
|
+
:PUNCTUATION,
|
|
187
|
+
:BANG,
|
|
188
|
+
:BRACKET,
|
|
189
|
+
:BACKTICK,
|
|
190
|
+
:STAR,
|
|
191
|
+
:UNDERSCORE,
|
|
192
|
+
:TILDE,
|
|
193
|
+
:AT,
|
|
194
|
+
:LT,
|
|
195
|
+
:DASH,
|
|
196
|
+
:CLASS_COUNT,
|
|
197
|
+
:BYTE_CLASSES,
|
|
198
|
+
:NOTHING,
|
|
199
|
+
:BOUNDARY,
|
|
200
|
+
:CHECK_AFTER,
|
|
201
|
+
:CHECK_BEFORE,
|
|
202
|
+
:ESCAPE_BEFORE,
|
|
203
|
+
:ESCAPE_AFTER,
|
|
204
|
+
:MERGING_CLASSES,
|
|
205
|
+
:FLANKING_CLASSES,
|
|
206
|
+
:HTML_START_BYTES,
|
|
207
|
+
:JOIN_ACTIONS
|
|
208
|
+
|
|
209
|
+
# Appends +piece+ to +buffer+ with what the join between them
|
|
210
|
+
# needs. On an empty buffer getbyte returns nil and there is no
|
|
211
|
+
# byte in front of the join, so the first piece needs no guard
|
|
212
|
+
# of its own.
|
|
213
|
+
#
|
|
214
|
+
# @param buffer [String] the Markdown written so far, changed in place
|
|
215
|
+
# @param piece [String] the next piece, not empty
|
|
216
|
+
# @return [String] +buffer+
|
|
217
|
+
def self.append(buffer, piece)
|
|
218
|
+
last_byte = buffer.getbyte(-1)
|
|
219
|
+
if last_byte
|
|
220
|
+
action =
|
|
221
|
+
JOIN_ACTIONS.getbyte((BYTE_CLASSES.getbyte(last_byte) << 8) + piece.getbyte(0))
|
|
222
|
+
join(buffer, piece, last_byte, action) unless action == NOTHING
|
|
223
|
+
end
|
|
224
|
+
buffer << piece
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Appends +piece+ to +buffer+ with the join that +like+ would need
|
|
228
|
+
# in its place: +like+ is the text a later step will most likely
|
|
229
|
+
# put there instead of +piece+ (see Tokens.placeholder). +before+,
|
|
230
|
+
# when given, stands in the same way for the end of the buffer,
|
|
231
|
+
# after a placeholder; then a `!` at its end can't take a
|
|
232
|
+
# backslash, and the boundary comment keeps it from making an
|
|
233
|
+
# image of a link instead.
|
|
234
|
+
#
|
|
235
|
+
# @param buffer [String] changed in place
|
|
236
|
+
# @param piece [String] the next piece, not empty
|
|
237
|
+
# @param like [String, nil] stands in for +piece+, nil for none
|
|
238
|
+
# @param before [String, nil] stands in for the end of +buffer+
|
|
239
|
+
# @return [String] +buffer+
|
|
240
|
+
def self.append_like(buffer, piece, like, before = nil)
|
|
241
|
+
tail = before || buffer
|
|
242
|
+
last_byte = tail.getbyte(-1)
|
|
243
|
+
if last_byte
|
|
244
|
+
after = like || piece
|
|
245
|
+
action =
|
|
246
|
+
JOIN_ACTIONS.getbyte((BYTE_CLASSES.getbyte(last_byte) << 8) + after.getbyte(0))
|
|
247
|
+
join(buffer, after, last_byte, action, tail, !like.nil?) unless action == NOTHING
|
|
248
|
+
end
|
|
249
|
+
buffer << piece
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Whether the last character of +string+ is escaped: only an odd
|
|
253
|
+
# run of backslashes in front of it escapes it, `\\!` is an
|
|
254
|
+
# escaped backslash followed by an active `!`. When the run
|
|
255
|
+
# reaches the start of the string the index turns negative and
|
|
256
|
+
# getbyte wraps around to the last character, which is no
|
|
257
|
+
# backslash and stops the loop.
|
|
258
|
+
#
|
|
259
|
+
# @param string [String] not empty
|
|
260
|
+
# @return [Boolean]
|
|
261
|
+
def self.escaped_end?(string)
|
|
262
|
+
escaped = false
|
|
263
|
+
index = string.bytesize - 2
|
|
264
|
+
while string.getbyte(index) == BACKSLASH
|
|
265
|
+
escaped = !escaped
|
|
266
|
+
index -= 1
|
|
267
|
+
end
|
|
268
|
+
escaped
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Carries out the action the table picked. NOTHING never gets
|
|
272
|
+
# here, the caller keeps it out, so the last branch is
|
|
273
|
+
# ESCAPE_BEFORE. +tail+ is what the end of the buffer is read
|
|
274
|
+
# as, and +stand_in+ says whether +piece+ stands for the piece
|
|
275
|
+
# that is appended, see .append_like: a stand-in gets no
|
|
276
|
+
# backslash, the comment keeps the two apart instead.
|
|
277
|
+
def self.join(buffer, piece, last_byte, action, tail = buffer, stand_in = false)
|
|
278
|
+
if action == BOUNDARY
|
|
279
|
+
buffer << BOUNDARY_COMMENT
|
|
280
|
+
elsif action == CHECK_AFTER
|
|
281
|
+
buffer << BOUNDARY_COMMENT if punctuation_at?(piece, after_run(piece))
|
|
282
|
+
elsif action == CHECK_BEFORE
|
|
283
|
+
buffer << BOUNDARY_COMMENT if punctuation_at?(tail, before_run(tail, last_byte))
|
|
284
|
+
elsif action == ESCAPE_AFTER
|
|
285
|
+
buffer << (stand_in ? BOUNDARY_COMMENT : "\\")
|
|
286
|
+
elsif !tail.equal?(buffer)
|
|
287
|
+
buffer << BOUNDARY_COMMENT
|
|
288
|
+
elsif !escaped_end?(buffer)
|
|
289
|
+
# Byte offsets: the `!` or `<` is the last byte.
|
|
290
|
+
buffer.bytesplice(buffer.bytesize - 1, 0, "\\")
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# The byte offset of the first byte after the run of the piece's
|
|
295
|
+
# first byte, nil when the piece is the run alone. A delimiter is
|
|
296
|
+
# ASCII, so the byte after the run starts a character.
|
|
297
|
+
def self.after_run(piece)
|
|
298
|
+
run = piece.getbyte(0)
|
|
299
|
+
index = 1
|
|
300
|
+
index += 1 while piece.getbyte(index) == run
|
|
301
|
+
index if index < piece.bytesize
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# The byte offset of the character in front of the run that ends
|
|
305
|
+
# the buffer, nil when the buffer is the run alone. The byte in
|
|
306
|
+
# front of the run may be inside a multi-byte character, so the
|
|
307
|
+
# search goes back to its first byte.
|
|
308
|
+
def self.before_run(buffer, run)
|
|
309
|
+
index = buffer.bytesize - 2
|
|
310
|
+
index -= 1 while index >= 0 && buffer.getbyte(index) == run
|
|
311
|
+
return if index.negative?
|
|
312
|
+
|
|
313
|
+
index -= 1 while index.positive? && (buffer.getbyte(index) & 0xC0) == 0x80
|
|
314
|
+
index
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Whether the character at byte +index+ of +string+ is punctuation
|
|
318
|
+
# for the flanking rules; nil is no character and no punctuation.
|
|
319
|
+
# An ASCII byte is answered from the class table, a character
|
|
320
|
+
# outside ASCII is matched as a whole.
|
|
321
|
+
def self.punctuation_at?(string, index)
|
|
322
|
+
return false if index.nil?
|
|
323
|
+
|
|
324
|
+
byte = string.getbyte(index)
|
|
325
|
+
return BYTE_CLASSES.getbyte(byte) >= PUNCTUATION if byte < 128
|
|
326
|
+
|
|
327
|
+
string.byteslice(index, char_length(byte)).match?(PUNCTUATION_AT)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Whether the character at byte +index+ of +string+ is whitespace
|
|
331
|
+
# as [[:space:]] reads it; nil is no character. A regex over the
|
|
332
|
+
# whole string would scan it for its encoding first, which costs
|
|
333
|
+
# as much as the string is long.
|
|
334
|
+
def self.space_at?(string, index)
|
|
335
|
+
return false if index.nil?
|
|
336
|
+
|
|
337
|
+
byte = string.getbyte(index)
|
|
338
|
+
return false if byte.nil?
|
|
339
|
+
return byte == SPACE || (byte >= TAB && byte <= CR) if byte < 128
|
|
340
|
+
return false if byte < 0xC2
|
|
341
|
+
|
|
342
|
+
string.byteslice(index, char_length(byte)).match?(SPACE_AT)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# The byte offset of the last character of +string+, nil for an
|
|
346
|
+
# empty string.
|
|
347
|
+
def self.last_char_index(string)
|
|
348
|
+
index = string.bytesize - 1
|
|
349
|
+
index -= 1 while index.positive? && (string.getbyte(index) & 0xC0) == 0x80
|
|
350
|
+
index.negative? ? nil : index
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Whether +string+ holds a character that is not whitespace.
|
|
354
|
+
def self.visible?(string)
|
|
355
|
+
index = 0
|
|
356
|
+
while (byte = string.getbyte(index))
|
|
357
|
+
return true unless space_at?(string, index)
|
|
358
|
+
|
|
359
|
+
index += byte < 128 ? 1 : char_length(byte)
|
|
360
|
+
end
|
|
361
|
+
false
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# The length in bytes of the character whose lead byte is +byte+.
|
|
365
|
+
# A slice that reaches into the next character is no valid
|
|
366
|
+
# string, so a slice is cut to the character.
|
|
367
|
+
def self.char_length(byte)
|
|
368
|
+
if byte >= 0xF0
|
|
369
|
+
4
|
|
370
|
+
elsif byte >= 0xE0
|
|
371
|
+
3
|
|
372
|
+
else
|
|
373
|
+
2
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
private_class_method :join, :punctuation_at?, :after_run, :before_run, :char_length
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|