• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / Penlight / 477

12 Feb 2024 09:01AM UTC coverage: 89.675% (+0.7%) from 88.938%
477

push

appveyor

web-flow
fix(doc): typo in example (#463)

5489 of 6121 relevant lines covered (89.67%)

346.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.45
/lua/pl/stringx.lua
1
--- Python-style extended string library.
2
--
3
-- see 3.6.1 of the Python reference.
4
-- If you want to make these available as string methods, then say
5
-- `stringx.import()` to bring them into the standard `string` table.
6
--
7
-- See @{03-strings.md|the Guide}
8
--
9
-- Dependencies: `pl.utils`, `pl.types`
10
-- @module pl.stringx
11
local utils = require 'pl.utils'
280✔
12
local is_callable = require 'pl.types'.is_callable
280✔
13
local string = string
280✔
14
local find = string.find
280✔
15
local type,setmetatable,ipairs = type,setmetatable,ipairs
280✔
16
local error = error
280✔
17
local gsub = string.gsub
280✔
18
local rep = string.rep
280✔
19
local sub = string.sub
280✔
20
local reverse = string.reverse
280✔
21
local concat = table.concat
280✔
22
local append = table.insert
280✔
23
local remove = table.remove
280✔
24
local escape = utils.escape
280✔
25
local ceil, max = math.ceil, math.max
280✔
26
local assert_arg,usplit = utils.assert_arg,utils.split
280✔
27
local lstrip
28
local unpack = utils.unpack
280✔
29
local pack = utils.pack
280✔
30

31
local function assert_string (n,s)
32
    assert_arg(n,s,'string')
3,785✔
33
end
34

35
local function non_empty(s)
36
    return #s > 0
96✔
37
end
38

39
local function assert_nonempty_string(n,s)
40
    assert_arg(n,s,'string',non_empty,'must be a non-empty string')
96✔
41
end
42

43
local function makelist(l)
44
    return setmetatable(l, require('pl.List'))
400✔
45
end
46

47
local stringx = {}
280✔
48

49
------------------
50
-- String Predicates
51
-- @section predicates
52

53
--- does s only contain alphabetic characters?
54
-- @string s a string
55
function stringx.isalpha(s)
280✔
56
    assert_string(1,s)
48✔
57
    return find(s,'^%a+$') == 1
48✔
58
end
59

60
--- does s only contain digits?
61
-- @string s a string
62
function stringx.isdigit(s)
280✔
63
    assert_string(1,s)
128✔
64
    return find(s,'^%d+$') == 1
128✔
65
end
66

67
--- does s only contain alphanumeric characters?
68
-- @string s a string
69
function stringx.isalnum(s)
280✔
70
    assert_string(1,s)
24✔
71
    return find(s,'^%w+$') == 1
24✔
72
end
73

74
--- does s only contain whitespace?
75
-- Matches on pattern '%s' so matches space, newline, tabs, etc.
76
-- @string s a string
77
function stringx.isspace(s)
280✔
78
    assert_string(1,s)
32✔
79
    return find(s,'^%s+$') == 1
32✔
80
end
81

82
--- does s only contain lower case characters?
83
-- @string s a string
84
function stringx.islower(s)
280✔
85
    assert_string(1,s)
32✔
86
    return find(s,'^[%l%s]+$') == 1
32✔
87
end
88

89
--- does s only contain upper case characters?
90
-- @string s a string
91
function stringx.isupper(s)
280✔
92
    assert_string(1,s)
32✔
93
    return find(s,'^[%u%s]+$') == 1
32✔
94
end
95

96
local function raw_startswith(s, prefix)
97
    return find(s,prefix,1,true) == 1
120✔
98
end
99

100
local function raw_endswith(s, suffix)
101
    return #s >= #suffix and find(s, suffix, #s-#suffix+1, true) and true or false
168✔
102
end
103

104
local function test_affixes(s, affixes, fn)
105
    if type(affixes) == 'string' then
256✔
106
        return fn(s,affixes)
208✔
107
    elseif type(affixes) == 'table' then
48✔
108
        for _,affix in ipairs(affixes) do
96✔
109
            if fn(s,affix) then return true end
120✔
110
        end
111
        return false
16✔
112
    else
113
        error(("argument #2 expected a 'string' or a 'table', got a '%s'"):format(type(affixes)))
×
114
    end
115
end
116

117
--- does s start with prefix or one of prefixes?
118
-- @string s a string
119
-- @param prefix a string or an array of strings
120
function stringx.startswith(s,prefix)
280✔
121
    assert_string(1,s)
104✔
122
    return test_affixes(s,prefix,raw_startswith)
104✔
123
end
124

125
--- does s end with suffix or one of suffixes?
126
-- @string s a string
127
-- @param suffix a string or an array of strings
128
function stringx.endswith(s,suffix)
280✔
129
    assert_string(1,s)
152✔
130
    return test_affixes(s,suffix,raw_endswith)
152✔
131
end
132

133
--- Strings and Lists
134
-- @section lists
135

136
--- concatenate the strings using this string as a delimiter.
137
-- Note that the arguments are reversed from `string.concat`.
138
-- @string s the string
139
-- @param seq a table of strings or numbers
140
-- @usage stringx.join(' ', {1,2,3}) == '1 2 3'
141
function stringx.join(s,seq)
280✔
142
    assert_string(1,s)
8✔
143
    return concat(seq,s)
8✔
144
end
145

146
--- Split a string into a list of lines.
147
-- `"\r"`, `"\n"`, and `"\r\n"` are considered line ends.
148
-- They are not included in the lines unless `keepends` is passed.
149
-- Terminal line end does not produce an extra line.
150
-- Splitting an empty string results in an empty list.
151
-- @string s the string.
152
-- @bool[opt] keep_ends include line ends.
153
-- @return List of lines
154
function stringx.splitlines(s, keep_ends)
280✔
155
    assert_string(1, s)
72✔
156
    local res = {}
72✔
157
    local pos = 1
72✔
158
    while true do
159
        local line_end_pos = find(s, '[\r\n]', pos)
184✔
160
        if not line_end_pos then
184✔
161
            break
45✔
162
        end
163

164
        local line_end = sub(s, line_end_pos, line_end_pos)
112✔
165
        if line_end == '\r' and sub(s, line_end_pos + 1, line_end_pos + 1) == '\n' then
132✔
166
            line_end = '\r\n'
16✔
167
        end
168

169
        local line = sub(s, pos, line_end_pos - 1)
112✔
170
        if keep_ends then
112✔
171
            line = line .. line_end
48✔
172
        end
173
        append(res, line)
112✔
174

175
        pos = line_end_pos + #line_end
112✔
176
    end
177

178
    if pos <= #s then
72✔
179
        append(res, sub(s, pos))
12✔
180
    end
181
    return makelist(res)
72✔
182
end
183

184
--- split a string into a list of strings using a delimiter.
185
-- @function split
186
-- @string s the string
187
-- @string[opt] re a delimiter (defaults to whitespace)
188
-- @int[opt] n maximum number of results
189
-- @return List
190
-- @usage #(stringx.split('one two')) == 2
191
-- @usage stringx.split('one,two,three', ',') == List{'one','two','three'}
192
-- @usage stringx.split('one,two,three', ',', 2) == List{'one','two,three'}
193
function stringx.split(s,re,n)
280✔
194
    assert_string(1,s)
224✔
195
    local plain = true
224✔
196
    if not re then -- default spaces
224✔
197
        s = lstrip(s)
240✔
198
        plain = false
160✔
199
    end
200
    local res = usplit(s,re,plain,n)
224✔
201
    if re and re ~= '' and
224✔
202
       find(s,re,-#re,true) and
48✔
203
       (n or math.huge) > #res then
24✔
204
        res[#res+1] = ""
16✔
205
    end
206
    return makelist(res)
224✔
207
end
208

209
--- replace all tabs in s with tabsize spaces. If not specified, tabsize defaults to 8.
210
-- Tab stops will be honored.
211
-- @string s the string
212
-- @int tabsize[opt=8] number of spaces to expand each tab
213
-- @return expanded string
214
-- @usage stringx.expandtabs('\tone,two,three', 4)   == '    one,two,three'
215
-- @usage stringx.expandtabs('  \tone,two,three', 4) == '    one,two,three'
216
function stringx.expandtabs(s,tabsize)
280✔
217
  assert_string(1,s)
56✔
218
  tabsize = tabsize or 8
56✔
219
  return (s:gsub("([^\t\r\n]*)\t", function(before_tab)
112✔
220
      if tabsize == 0 then
48✔
221
        return before_tab
8✔
222
      else
223
        return before_tab .. (" "):rep(tabsize - #before_tab % tabsize)
50✔
224
      end
225
    end))
226
end
227

228
--- Finding and Replacing
229
-- @section find
230

231
local function _find_all(s,sub,first,last,allow_overlap)
232
    first = first or 1
176✔
233
    last = last or #s
176✔
234
    if sub == '' then return last+1,last-first+1 end
176✔
235
    local i1,i2 = find(s,sub,first,true)
144✔
236
    local res
237
    local k = 0
144✔
238
    while i1 do
344✔
239
        if last and i2 > last then break end
224✔
240
        res = i1
200✔
241
        k = k + 1
200✔
242
        if allow_overlap then
200✔
243
            i1,i2 = find(s,sub,i1+1,true)
152✔
244
        else
245
            i1,i2 = find(s,sub,i2+1,true)
48✔
246
        end
247
    end
248
    return res,k
144✔
249
end
250

251
--- find index of first instance of sub in s from the left.
252
-- @string s the string
253
-- @string sub substring
254
-- @int[opt] first first index
255
-- @int[opt] last last index
256
-- @return start index, or nil if not found
257
function stringx.lfind(s,sub,first,last)
280✔
258
    assert_string(1,s)
128✔
259
    assert_string(2,sub)
128✔
260
    local i1, i2 = find(s,sub,first,true)
128✔
261

262
    if i1 and (not last or i2 <= last) then
128✔
263
        return i1
96✔
264
    else
265
        return nil
32✔
266
    end
267
end
268

269
--- find index of first instance of sub in s from the right.
270
-- @string s the string
271
-- @string sub substring
272
-- @int[opt] first first index
273
-- @int[opt] last last index
274
-- @return start index, or nil if not found
275
function stringx.rfind(s,sub,first,last)
280✔
276
    assert_string(1,s)
120✔
277
    assert_string(2,sub)
120✔
278
    return (_find_all(s,sub,first,last,true))
180✔
279
end
280

281
--- replace up to n instances of old by new in the string s.
282
-- If n is not present, replace all instances.
283
-- @string s the string
284
-- @string old the target substring
285
-- @string new the substitution
286
-- @int[opt] n optional maximum number of substitutions
287
-- @return result string
288
function stringx.replace(s,old,new,n)
280✔
289
    assert_string(1,s)
72✔
290
    assert_string(2,old)
72✔
291
    assert_string(3,new)
72✔
292
    return (gsub(s,escape(old),new:gsub('%%','%%%%'),n))
108✔
293
end
294

295
--- count all instances of substring in string.
296
-- @string s the string
297
-- @string sub substring
298
-- @bool[opt] allow_overlap allow matches to overlap
299
-- @usage
300
-- assert(stringx.count('banana', 'ana') == 1)
301
-- assert(stringx.count('banana', 'ana', true) == 2)
302
function stringx.count(s,sub,allow_overlap)
280✔
303
    assert_string(1,s)
56✔
304
    local _,k = _find_all(s,sub,1,false,allow_overlap)
56✔
305
    return k
56✔
306
end
307

308
--- Stripping and Justifying
309
-- @section strip
310

311
local function _just(s,w,ch,left,right)
312
    local n = #s
128✔
313
    if w > n then
128✔
314
        if not ch then ch = ' ' end
80✔
315
        local f1,f2
316
        if left and right then
80✔
317
            local rn = ceil((w-n)/2)
32✔
318
            local ln = w - n - rn
32✔
319
            f1 = rep(ch,ln)
40✔
320
            f2 = rep(ch,rn)
40✔
321
        elseif right then
48✔
322
            f1 = rep(ch,w-n)
30✔
323
            f2 = ''
24✔
324
        else
325
            f2 = rep(ch,w-n)
30✔
326
            f1 = ''
24✔
327
        end
328
        return f1..s..f2
80✔
329
    else
330
        return s
48✔
331
    end
332
end
333

334
--- left-justify s with width w.
335
-- @string s the string
336
-- @int w width of justification
337
-- @string[opt=' '] ch padding character
338
-- @usage stringx.ljust('hello', 10, '*') == '*****hello'
339
function stringx.ljust(s,w,ch)
280✔
340
    assert_string(1,s)
40✔
341
    assert_arg(2,w,'number')
40✔
342
    return _just(s,w,ch,true,false)
40✔
343
end
344

345
--- right-justify s with width w.
346
-- @string s the string
347
-- @int w width of justification
348
-- @string[opt=' '] ch padding character
349
-- @usage stringx.rjust('hello', 10, '*') == 'hello*****'
350
function stringx.rjust(s,w,ch)
280✔
351
    assert_string(1,s)
40✔
352
    assert_arg(2,w,'number')
40✔
353
    return _just(s,w,ch,false,true)
40✔
354
end
355

356
--- center-justify s with width w.
357
-- @string s the string
358
-- @int w width of justification
359
-- @string[opt=' '] ch padding character
360
-- @usage stringx.center('hello', 10, '*') == '**hello***'
361
function stringx.center(s,w,ch)
280✔
362
    assert_string(1,s)
48✔
363
    assert_arg(2,w,'number')
48✔
364
    return _just(s,w,ch,true,true)
48✔
365
end
366

367
local function _strip(s,left,right,chrs)
368
    if not chrs then
1,272✔
369
        chrs = '%s'
1,248✔
370
    else
371
        chrs = '['..escape(chrs)..']'
36✔
372
    end
373
    local f = 1
1,272✔
374
    local t
375
    if left then
1,272✔
376
        local i1,i2 = find(s,'^'..chrs..'*')
1,184✔
377
        if i2 >= i1 then
1,184✔
378
            f = i2+1
176✔
379
        end
380
    end
381
    if right then
1,272✔
382
        if #s < 200 then
1,024✔
383
            local i1,i2 = find(s,chrs..'*$',f)
1,008✔
384
            if i2 >= i1 then
1,008✔
385
                t = i1-1
152✔
386
            end
387
        else
388
            local rs = reverse(s)
16✔
389
            local i1,i2 = find(rs, '^'..chrs..'*')
16✔
390
            if i2 >= i1 then
16✔
391
                t = -i2-1
×
392
            end
393
        end
394
    end
395
    return sub(s,f,t)
1,272✔
396
end
397

398
--- trim any characters on the left of s.
399
-- @string s the string
400
-- @string[opt='%s'] chrs default any whitespace character,
401
-- but can be a string of characters to be trimmed
402
function stringx.lstrip(s,chrs)
280✔
403
    assert_string(1,s)
248✔
404
    return _strip(s,true,false,chrs)
248✔
405
end
406
lstrip = stringx.lstrip
280✔
407

408
--- trim any characters on the right of s.
409
-- @string s the string
410
-- @string[opt='%s'] chrs default any whitespace character,
411
-- but can be a string of characters to be trimmed
412
function stringx.rstrip(s,chrs)
280✔
413
    assert_string(1,s)
88✔
414
    return _strip(s,false,true,chrs)
88✔
415
end
416

417
--- trim any characters on both left and right of s.
418
-- @string s the string
419
-- @string[opt='%s'] chrs default any whitespace character,
420
-- but can be a string of characters to be trimmed
421
-- @usage stringx.strip('  --== Hello ==--  ', "- =")  --> 'Hello'
422
function stringx.strip(s,chrs)
280✔
423
    assert_string(1,s)
936✔
424
    return _strip(s,true,true,chrs)
936✔
425
end
426

427
--- Partitioning Strings
428
-- @section partitioning
429

430
--- split a string using a pattern. Note that at least one value will be returned!
431
-- @string s the string
432
-- @string[opt='%s'] re a Lua string pattern (defaults to whitespace)
433
-- @return the parts of the string
434
-- @usage  a,b = line:splitv('=')
435
-- @see utils.splitv
436
function stringx.splitv(s,re)
280✔
437
    assert_string(1,s)
8✔
438
    return utils.splitv(s,re)
8✔
439
end
440

441
-- The partition functions split a string using a delimiter into three parts:
442
-- the part before, the delimiter itself, and the part afterwards
443
local function _partition(p,delim,fn)
444
    local i1,i2 = fn(p,delim)
80✔
445
    if not i1 or i1 == -1 then
80✔
446
        return p,'',''
24✔
447
    else
448
        if not i2 then i2 = i1 end
56✔
449
        return sub(p,1,i1-1),sub(p,i1,i2),sub(p,i2+1)
140✔
450
    end
451
end
452

453
--- partition the string using first occurrence of a delimiter
454
-- @string s the string
455
-- @string ch delimiter (match as plain string, no patterns)
456
-- @return part before ch
457
-- @return ch
458
-- @return part after ch
459
-- @usage {stringx.partition('a,b,c', ','))} == {'a', ',', 'b,c'}
460
-- @usage {stringx.partition('abc', 'x'))} == {'abc', '', ''}
461
function stringx.partition(s,ch)
280✔
462
    assert_string(1,s)
56✔
463
    assert_nonempty_string(2,ch)
56✔
464
    return _partition(s,ch,stringx.lfind)
48✔
465
end
466

467
--- partition the string p using last occurrence of a delimiter
468
-- @string s the string
469
-- @string ch delimiter (match as plain string, no patterns)
470
-- @return part before ch
471
-- @return ch
472
-- @return part after ch
473
-- @usage {stringx.rpartition('a,b,c', ','))} == {'a,b', ',', 'c'}
474
-- @usage {stringx.rpartition('abc', 'x'))} == {'', '', 'abc'}
475
function stringx.rpartition(s,ch)
280✔
476
    assert_string(1,s)
40✔
477
    assert_nonempty_string(2,ch)
40✔
478
    local a,b,c = _partition(s,ch,stringx.rfind)
32✔
479
    if a == s then -- no match found
32✔
480
        return c,b,a
8✔
481
    end
482
    return a,b,c
24✔
483
end
484

485
--- return the 'character' at the index.
486
-- @string s the string
487
-- @int idx an index (can be negative)
488
-- @return a substring of length 1 if successful, empty string otherwise.
489
function stringx.at(s,idx)
280✔
490
    assert_string(1,s)
32✔
491
    assert_arg(2,idx,'number')
32✔
492
    return sub(s,idx,idx)
32✔
493
end
494

495

496
--- Text handling
497
-- @section text
498

499

500
--- indent a multiline string.
501
-- @tparam string s the (multiline) string
502
-- @tparam integer n the size of the indent
503
-- @tparam[opt=' '] string ch the character to use when indenting
504
-- @return indented string
505
function stringx.indent (s,n,ch)
280✔
506
  assert_arg(1,s,'string')
32✔
507
  assert_arg(2,n,'number')
32✔
508
  local lines = usplit(s ,'\n')
32✔
509
  local prefix = string.rep(ch or ' ',n)
32✔
510
  for i, line in ipairs(lines) do
104✔
511
    lines[i] = prefix..line
72✔
512
  end
513
  return concat(lines,'\n')..'\n'
32✔
514
end
515

516

517
--- dedent a multiline string by removing any initial indent.
518
-- useful when working with [[..]] strings.
519
-- Empty lines are ignored.
520
-- @tparam string s the (multiline) string
521
-- @return a string with initial indent zero.
522
-- @usage
523
-- local s = dedent [[
524
--          One
525
--
526
--        Two
527
--
528
--      Three
529
-- ]]
530
-- assert(s == [[
531
--     One
532
--
533
--   Two
534
--
535
-- Three
536
-- ]])
537
function stringx.dedent (s)
280✔
538
  assert_arg(1,s,'string')
32✔
539
  local lst = usplit(s,'\n')
32✔
540
  if #lst>0 then
32✔
541
    local ind_size = math.huge
32✔
542
    for i, line in ipairs(lst) do
128✔
543
      local i1, i2 = lst[i]:find('^%s*[^%s]')
96✔
544
      if i1 and i2 < ind_size then
96✔
545
        ind_size = i2
48✔
546
      end
547
    end
548
    for i, line in ipairs(lst) do
128✔
549
      lst[i] = lst[i]:sub(ind_size, -1)
144✔
550
    end
551
  end
552
  return concat(lst,'\n')..'\n'
32✔
553
end
554

555

556

557
do
558
  local buildline = function(words, size, breaklong)
559
    -- if overflow is set, a word longer than size, will overflow the size
560
    -- otherwise it will be chopped in line-length pieces
561
    local line = {}
720✔
562
    if #words[1] > size then
720✔
563
      -- word longer than line
564
      if not breaklong then
280✔
565
        line[1] = words[1]
104✔
566
        remove(words, 1)
130✔
567
      else
568
        line[1] = words[1]:sub(1, size)
264✔
569
        words[1] = words[1]:sub(size + 1, -1)
264✔
570
      end
571
    else
572
      local len = 0
440✔
573
      while words[1] and (len + #words[1] <= size) or
1,440✔
574
            (len == 0 and #words[1] == size) do
940✔
575
        if words[1] ~= "" then
1,000✔
576
          line[#line+1] = words[1]
920✔
577
          len = len + #words[1] + 1
920✔
578
        end
579
        remove(words, 1)
1,250✔
580
      end
581
    end
582
    return stringx.strip(concat(line, " ")), words
1,080✔
583
  end
584

585
  --- format a paragraph into lines so that they fit into a line width.
586
  -- It will not break long words by default, so lines can be over the length
587
  -- to that extent.
588
  -- @tparam string s the string to format
589
  -- @tparam[opt=70] integer width the margin width
590
  -- @tparam[opt=false] boolean breaklong if truthy, words longer than the width given will be forced split.
591
  -- @return a list of lines (List object), use `fill` to return a string instead of a `List`.
592
  -- @see pl.List
593
  -- @see fill
594
  stringx.wrap = function(s, width, breaklong)
595
    s = s:gsub('\n',' ') -- remove line breaks
120✔
596
    s = stringx.strip(s) -- remove leading/trailing whitespace
180✔
597
    if s == "" then
120✔
598
      return { "" }
16✔
599
    end
600
    width = width or 70
104✔
601
    local out = {}
104✔
602
    local words = usplit(s, "%s")
104✔
603
    while words[1] do
824✔
604
      out[#out+1], words = buildline(words, width, breaklong)
1,080✔
605
    end
606
    return makelist(out)
104✔
607
  end
608
end
609

610
--- format a paragraph so that it fits into a line width.
611
-- @tparam string s the string to format
612
-- @tparam[opt=70] integer width the margin width
613
-- @tparam[opt=false] boolean breaklong if truthy, words longer than the width given will be forced split.
614
-- @return a string, use `wrap` to return a list of lines instead of a string.
615
-- @see wrap
616
function stringx.fill (s,width,breaklong)
280✔
617
  return concat(stringx.wrap(s,width,breaklong),'\n') .. '\n'
12✔
618
end
619

620
--- Template
621
-- @section Template
622

623

624
local function _substitute(s,tbl,safe)
625
  local subst
626
  if is_callable(tbl) then
36✔
627
    subst = tbl
×
628
  else
629
    function subst(f)
21✔
630
      local s = tbl[f]
32✔
631
      if not s then
32✔
632
        if safe then
×
633
          return f
×
634
        else
635
          error("not present in table "..f)
×
636
        end
637
      else
638
        return s
32✔
639
      end
640
    end
641
  end
642
  local res = gsub(s,'%${([%w_]+)}',subst)
24✔
643
  return (gsub(res,'%$([%w_]+)',subst))
24✔
644
end
645

646

647

648
local Template = {}
280✔
649
stringx.Template = Template
280✔
650
Template.__index = Template
280✔
651
setmetatable(Template, {
560✔
652
  __call = function(obj,tmpl)
653
    return Template.new(tmpl)
24✔
654
  end
655
})
656

657
--- Creates a new Template class.
658
-- This is a shortcut to `Template.new(tmpl)`.
659
-- @tparam string tmpl the template string
660
-- @function Template
661
-- @treturn Template
662
function Template.new(tmpl)
280✔
663
  assert_arg(1,tmpl,'string')
24✔
664
  local res = {}
24✔
665
  res.tmpl = tmpl
24✔
666
  setmetatable(res,Template)
24✔
667
  return res
24✔
668
end
669

670
--- substitute values into a template, throwing an error.
671
-- This will throw an error if no name is found.
672
-- @tparam table tbl a table of name-value pairs.
673
-- @return string with place holders substituted
674
function Template:substitute(tbl)
280✔
675
  assert_arg(1,tbl,'table')
16✔
676
  return _substitute(self.tmpl,tbl,false)
16✔
677
end
678

679
--- substitute values into a template.
680
-- This version just passes unknown names through.
681
-- @tparam table tbl a table of name-value pairs.
682
-- @return string with place holders substituted
683
function Template:safe_substitute(tbl)
280✔
684
  assert_arg(1,tbl,'table')
×
685
  return _substitute(self.tmpl,tbl,true)
×
686
end
687

688
--- substitute values into a template, preserving indentation. <br>
689
-- If the value is a multiline string _or_ a template, it will insert
690
-- the lines at the correct indentation. <br>
691
-- Furthermore, if a template, then that template will be substituted
692
-- using the same table.
693
-- @tparam table tbl a table of name-value pairs.
694
-- @return string with place holders substituted
695
function Template:indent_substitute(tbl)
280✔
696
  assert_arg(1,tbl,'table')
8✔
697
  if not self.strings then
8✔
698
    self.strings = usplit(self.tmpl,'\n')
12✔
699
  end
700

701
  -- the idea is to substitute line by line, grabbing any spaces as
702
  -- well as the $var. If the value to be substituted contains newlines,
703
  -- then we split that into lines and adjust the indent before inserting.
704
  local function subst(line)
705
    return line:gsub('(%s*)%$([%w_]+)',function(sp,f)
36✔
706
      local subtmpl
707
      local s = tbl[f]
8✔
708
      if not s then error("not present in table "..f) end
8✔
709
      if getmetatable(s) == Template then
8✔
710
        subtmpl = s
×
711
        s = s.tmpl
×
712
      else
713
        s = tostring(s)
8✔
714
      end
715
      if s:find '\n' then
8✔
716
        local lines = usplit(s, '\n')
8✔
717
        for i, line in ipairs(lines) do
32✔
718
          lines[i] = sp..line
24✔
719
        end
720
        s = concat(lines, '\n') .. '\n'
8✔
721
      end
722
      if subtmpl then
8✔
723
        return _substitute(s, tbl)
×
724
      else
725
        return s
8✔
726
      end
727
    end)
728
  end
729

730
  local lines = {}
8✔
731
  for i, line in ipairs(self.strings) do
32✔
732
    lines[i] = subst(line)
36✔
733
  end
734
  return concat(lines,'\n')..'\n'
8✔
735
end
736

737

738

739
--- Miscellaneous
740
-- @section misc
741

742
--- return an iterator over all lines in a string
743
-- @string s the string
744
-- @return an iterator
745
-- @usage
746
-- local line_no = 1
747
-- for line in stringx.lines(some_text) do
748
--   print(line_no, line)
749
--   line_no = line_no + 1
750
-- end
751
function stringx.lines(s)
280✔
752
    assert_string(1,s)
24✔
753
    if not s:find '\n$' then s = s..'\n' end
24✔
754
    return s:gmatch('([^\n]*)\n')
24✔
755
end
756

757
--- initial word letters uppercase ('title case').
758
-- Here 'words' mean chunks of non-space characters.
759
-- @string s the string
760
-- @return a string with each word's first letter uppercase
761
-- @usage stringx.title("hello world") == "Hello World")
762
function stringx.title(s)
280✔
763
    assert_string(1,s)
24✔
764
    return (s:gsub('(%S)(%S*)',function(f,r)
48✔
765
        return f:upper()..r:lower()
64✔
766
    end))
767
end
768

769
stringx.capitalize = stringx.title
280✔
770

771
do
772
  local ellipsis = '...'
280✔
773
  local n_ellipsis = #ellipsis
280✔
774

775
  --- Return a shortened version of a string.
776
  -- Fits string within w characters. Removed characters are marked with ellipsis.
777
  -- @string s the string
778
  -- @int w the maximum size allowed
779
  -- @bool tail true if we want to show the end of the string (head otherwise)
780
  -- @usage ('1234567890'):shorten(8) == '12345...'
781
  -- @usage ('1234567890'):shorten(8, true) == '...67890'
782
  -- @usage ('1234567890'):shorten(20) == '1234567890'
783
  function stringx.shorten(s,w,tail)
280✔
784
      assert_string(1,s)
144✔
785
      if #s > w then
144✔
786
          if w < n_ellipsis then return ellipsis:sub(1,w) end
88✔
787
          if tail then
40✔
788
              local i = #s - w + 1 + n_ellipsis
16✔
789
              return ellipsis .. s:sub(i)
24✔
790
          else
791
              return s:sub(1,w-n_ellipsis) .. ellipsis
36✔
792
          end
793
      end
794
      return s
56✔
795
  end
796
end
797

798

799
do
800
  -- Utility function that finds any patterns that match a long string's an open or close.
801
  -- Note that having this function use the least number of equal signs that is possible is a harder algorithm to come up with.
802
  -- Right now, it simply returns the greatest number of them found.
803
  -- @param s The string
804
  -- @return 'nil' if not found. If found, the maximum number of equal signs found within all matches.
805
  local function has_lquote(s)
806
      local lstring_pat = '([%[%]])(=*)%1'
377✔
807
      local equals, new_equals, _
808
      local finish = 1
377✔
809
      repeat
810
          _, finish, _, new_equals = s:find(lstring_pat, finish)
769✔
811
          if new_equals then
769✔
812
              equals = max(equals or 0, #new_equals)
392✔
813
          end
814
      until not new_equals
769✔
815

816
      return equals
377✔
817
  end
818

819
  --- Quote the given string and preserve any control or escape characters, such that reloading the string in Lua returns the same result.
820
  -- @param s The string to be quoted.
821
  -- @return The quoted string.
822
  function stringx.quote_string(s)
280✔
823
      assert_string(1,s)
377✔
824
      -- Find out if there are any embedded long-quote sequences that may cause issues.
825
      -- This is important when strings are embedded within strings, like when serializing.
826
      -- Append a closing bracket to catch unfinished long-quote sequences at the end of the string.
827
      local equal_signs = has_lquote(s .. "]")
377✔
828

829
      -- Note that strings containing "\r" can't be quoted using long brackets
830
      -- as Lua lexer converts all newlines to "\n" within long strings.
831
      if (s:find("\n") or equal_signs) and not s:find("\r") then
377✔
832
          -- If there is an embedded sequence that matches a long quote, then
833
          -- find the one with the maximum number of = signs and add one to that number.
834
          equal_signs = ("="):rep((equal_signs or -1) + 1)
170✔
835
          -- Long strings strip out leading newline. We want to retain that, when quoting.
836
          if s:find("^\n") then s = "\n" .. s end
136✔
837
          local lbracket, rbracket =
838
              "[" .. equal_signs .. "[",
136✔
839
              "]" .. equal_signs .. "]"
136✔
840
          s = lbracket .. s .. rbracket
136✔
841
      else
842
          -- Escape funny stuff. Lua 5.1 does not handle "\r" correctly.
843
          s = ("%q"):format(s):gsub("\r", "\\r")
241✔
844
      end
845
      return s
377✔
846
  end
847
end
848

849

850
--- Python-style formatting operator.
851
-- Calling `text.format_operator()` overloads the % operator for strings to give
852
-- Python/Ruby style formatted output.
853
-- This is extended to also do template-like substitution for map-like data.
854
--
855
-- Note this goes further than the original, and will allow these cases:
856
--
857
-- 1. a single value
858
-- 2. a list of values
859
-- 3. a map of var=value pairs
860
-- 4. a function, as in gsub
861
--
862
-- For the second two cases, it uses $-variable substitution.
863
--
864
-- When called, this function will monkey-patch the global `string` metatable by
865
-- adding a `__mod` method.
866
--
867
-- See <a href="http://lua-users.org/wiki/StringInterpolation">the lua-users wiki</a>
868
--
869
-- @usage
870
-- require 'pl.text'.format_operator()
871
-- local out1 = '%s = %5.3f' % {'PI',math.pi}                   --> 'PI = 3.142'
872
-- local out2 = '$name = $value' % {name='dog',value='Pluto'}   --> 'dog = Pluto'
873
function stringx.format_operator()
280✔
874

875
  local format = string.format
8✔
876

877
  -- a more forgiving version of string.format, which applies
878
  -- tostring() to any value with a %s format.
879
  local function formatx (fmt,...)
880
    local args = pack(...)
24✔
881
    local i = 1
24✔
882
    for p in fmt:gmatch('%%.') do
56✔
883
      if p == '%s' and type(args[i]) ~= 'string' then
32✔
884
        args[i] = tostring(args[i])
12✔
885
      end
886
      i = i + 1
32✔
887
    end
888
    return format(fmt,unpack(args))
36✔
889
  end
890

891
  local function basic_subst(s,t)
892
    return (s:gsub('%$([%w_]+)',t))
8✔
893
  end
894

895
  getmetatable("").__mod = function(a, b)
8✔
896
    if b == nil then
40✔
897
      return a
×
898
    elseif type(b) == "table" and getmetatable(b) == nil then
40✔
899
      if #b == 0 then -- assume a map-like table
16✔
900
        return _substitute(a,b,true)
8✔
901
      else
902
        return formatx(a,unpack(b))
12✔
903
      end
904
    elseif type(b) == 'function' then
24✔
905
      return basic_subst(a,b)
8✔
906
    else
907
      return formatx(a,b)
16✔
908
    end
909
  end
910
end
911

912
--- import the stringx functions into the global string (meta)table
913
function stringx.import()
280✔
914
    utils.import(stringx,string)
×
915
end
916

917
return stringx
280✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc