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

lunarmodules / Penlight / 21340975460

25 Jan 2026 10:51PM UTC coverage: 90.15%. Remained the same
21340975460

push

github

web-flow
Merge 04b8b46aa into 678de0ebb

5537 of 6142 relevant lines covered (90.15%)

727.38 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'
595✔
12
local is_callable = require 'pl.types'.is_callable
595✔
13
local string = string
595✔
14
local find = string.find
595✔
15
local type,setmetatable,ipairs = type,setmetatable,ipairs
595✔
16
local error = error
595✔
17
local gsub = string.gsub
595✔
18
local rep = string.rep
595✔
19
local sub = string.sub
595✔
20
local reverse = string.reverse
595✔
21
local concat = table.concat
595✔
22
local append = table.insert
595✔
23
local remove = table.remove
595✔
24
local escape = utils.escape
595✔
25
local ceil, max = math.ceil, math.max
595✔
26
local assert_arg,usplit = utils.assert_arg,utils.split
595✔
27
local lstrip
28
local unpack = utils.unpack
595✔
29
local pack = utils.pack
595✔
30

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

35
local function non_empty(s)
36
    return #s > 0
204✔
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')
204✔
41
end
42

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

47
local stringx = {}
595✔
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)
595✔
56
    assert_string(1,s)
102✔
57
    return find(s,'^%a+$') == 1
102✔
58
end
59

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

67
--- does s only contain alphanumeric characters?
68
-- @string s a string
69
function stringx.isalnum(s)
595✔
70
    assert_string(1,s)
51✔
71
    return find(s,'^%w+$') == 1
51✔
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)
595✔
78
    assert_string(1,s)
68✔
79
    return find(s,'^%s+$') == 1
68✔
80
end
81

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

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

96
local function raw_startswith(s, prefix)
97
    return find(s,prefix,1,true) == 1
255✔
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
357✔
102
end
103

104
local function test_affixes(s, affixes, fn)
105
    if type(affixes) == 'string' then
544✔
106
        return fn(s,affixes)
442✔
107
    elseif type(affixes) == 'table' then
102✔
108
        for _,affix in ipairs(affixes) do
204✔
109
            if fn(s,affix) then return true end
220✔
110
        end
111
        return false
34✔
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)
595✔
121
    assert_string(1,s)
221✔
122
    return test_affixes(s,prefix,raw_startswith)
221✔
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)
595✔
129
    assert_string(1,s)
323✔
130
    return test_affixes(s,suffix,raw_endswith)
323✔
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)
595✔
142
    assert_string(1,s)
17✔
143
    return concat(seq,s)
17✔
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)
595✔
155
    assert_string(1, s)
153✔
156
    local res = {}
153✔
157
    local pos = 1
153✔
158
    while true do
159
        local line_end_pos = find(s, '[\r\n]', pos)
391✔
160
        if not line_end_pos then
391✔
161
            break
72✔
162
        end
163

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

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

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

178
    if pos <= #s then
153✔
179
        append(res, sub(s, pos))
22✔
180
    end
181
    return makelist(res)
153✔
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)
595✔
194
    assert_string(1,s)
476✔
195
    local plain = true
476✔
196
    if not re then -- default spaces
476✔
197
        s = lstrip(s)
440✔
198
        plain = false
340✔
199
    end
200
    local res = usplit(s,re,plain,n)
476✔
201
    if re and re ~= '' and
476✔
202
       find(s,re,-#re,true) and
102✔
203
       (n or math.huge) > #res then
51✔
204
        res[#res+1] = ""
34✔
205
    end
206
    return makelist(res)
476✔
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 (multi-line strings are supported)
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)
595✔
217
  assert_string(1,s)
136✔
218
  tabsize = tabsize or 8
136✔
219
  return (s:gsub("([^\t\r\n]*)\t", function(before_tab)
272✔
220
      if tabsize == 0 then
153✔
221
        return before_tab
17✔
222
      else
223
        return before_tab .. (" "):rep(tabsize - #before_tab % tabsize)
136✔
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
374✔
233
    last = last or #s
374✔
234
    if sub == '' then return last+1,last-first+1 end
374✔
235
    local i1,i2 = find(s,sub,first,true)
306✔
236
    local res
237
    local k = 0
306✔
238
    while i1 do
731✔
239
        if last and i2 > last then break end
476✔
240
        res = i1
425✔
241
        k = k + 1
425✔
242
        if allow_overlap then
425✔
243
            i1,i2 = find(s,sub,i1+1,true)
323✔
244
        else
245
            i1,i2 = find(s,sub,i2+1,true)
102✔
246
        end
247
    end
248
    return res,k
306✔
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)
595✔
258
    assert_string(1,s)
272✔
259
    assert_string(2,sub)
272✔
260
    local i1, i2 = find(s,sub,first,true)
272✔
261

262
    if i1 and (not last or i2 <= last) then
272✔
263
        return i1
204✔
264
    else
265
        return nil
68✔
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)
595✔
276
    assert_string(1,s)
255✔
277
    assert_string(2,sub)
255✔
278
    return (_find_all(s,sub,first,last,true))
330✔
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)
595✔
289
    assert_string(1,s)
153✔
290
    assert_string(2,old)
153✔
291
    assert_string(3,new)
153✔
292
    return (gsub(s,escape(old),new:gsub('%%','%%%%'),n))
198✔
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)
595✔
303
    assert_string(1,s)
119✔
304
    local _,k = _find_all(s,sub,1,false,allow_overlap)
119✔
305
    return k
119✔
306
end
307

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

311
local function _just(s,w,ch,left,right)
312
    local n = #s
272✔
313
    if w > n then
272✔
314
        if not ch then ch = ' ' end
170✔
315
        local f1,f2
316
        if left and right then
170✔
317
            local rn = ceil((w-n)/2)
68✔
318
            local ln = w - n - rn
68✔
319
            f1 = rep(ch,ln)
68✔
320
            f2 = rep(ch,rn)
68✔
321
        elseif right then
102✔
322
            f1 = rep(ch,w-n)
51✔
323
            f2 = ''
51✔
324
        else
325
            f2 = rep(ch,w-n)
51✔
326
            f1 = ''
51✔
327
        end
328
        return f1..s..f2
170✔
329
    else
330
        return s
102✔
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)
595✔
340
    assert_string(1,s)
85✔
341
    assert_arg(2,w,'number')
85✔
342
    return _just(s,w,ch,true,false)
85✔
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)
595✔
351
    assert_string(1,s)
85✔
352
    assert_arg(2,w,'number')
85✔
353
    return _just(s,w,ch,false,true)
85✔
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)
595✔
362
    assert_string(1,s)
102✔
363
    assert_arg(2,w,'number')
102✔
364
    return _just(s,w,ch,true,true)
102✔
365
end
366

367
local function _strip(s,left,right,chrs)
368
    if not chrs then
2,703✔
369
        chrs = '%s'
2,652✔
370
    else
371
        chrs = '['..escape(chrs)..']'
66✔
372
    end
373
    local f = 1
2,703✔
374
    local t
375
    if left then
2,703✔
376
        local i1,i2 = find(s,'^'..chrs..'*')
2,516✔
377
        if i2 >= i1 then
2,516✔
378
            f = i2+1
374✔
379
        end
380
    end
381
    if right then
2,703✔
382
        if #s < 200 then
2,176✔
383
            local i1,i2 = find(s,chrs..'*$',f)
2,142✔
384
            if i2 >= i1 then
2,142✔
385
                t = i1-1
323✔
386
            end
387
        else
388
            local rs = reverse(s)
34✔
389
            local i1,i2 = find(rs, '^'..chrs..'*')
34✔
390
            if i2 >= i1 then
34✔
391
                t = -i2-1
×
392
            end
393
        end
394
    end
395
    return sub(s,f,t)
2,703✔
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)
595✔
403
    assert_string(1,s)
527✔
404
    return _strip(s,true,false,chrs)
527✔
405
end
406
lstrip = stringx.lstrip
595✔
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)
595✔
413
    assert_string(1,s)
187✔
414
    return _strip(s,false,true,chrs)
187✔
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)
595✔
423
    assert_string(1,s)
1,989✔
424
    return _strip(s,true,true,chrs)
1,989✔
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)
595✔
437
    assert_string(1,s)
17✔
438
    return utils.splitv(s,re)
17✔
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)
170✔
445
    if not i1 or i1 == -1 then
170✔
446
        return p,'',''
51✔
447
    else
448
        if not i2 then i2 = i1 end
119✔
449
        return sub(p,1,i1-1),sub(p,i1,i2),sub(p,i2+1)
216✔
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)
595✔
462
    assert_string(1,s)
119✔
463
    assert_nonempty_string(2,ch)
119✔
464
    return _partition(s,ch,stringx.lfind)
102✔
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)
595✔
476
    assert_string(1,s)
85✔
477
    assert_nonempty_string(2,ch)
85✔
478
    local a,b,c = _partition(s,ch,stringx.rfind)
68✔
479
    if a == s then -- no match found
68✔
480
        return c,b,a
17✔
481
    end
482
    return a,b,c
51✔
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)
595✔
490
    assert_string(1,s)
68✔
491
    assert_arg(2,idx,'number')
68✔
492
    return sub(s,idx,idx)
68✔
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)
595✔
506
  assert_arg(1,s,'string')
68✔
507
  assert_arg(2,n,'number')
68✔
508
  local lines = usplit(s ,'\n')
68✔
509
  local prefix = string.rep(ch or ' ',n)
68✔
510
  for i, line in ipairs(lines) do
221✔
511
    lines[i] = prefix..line
153✔
512
  end
513
  return concat(lines,'\n')..'\n'
68✔
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)
595✔
538
  assert_arg(1,s,'string')
68✔
539
  local lst = usplit(s,'\n')
68✔
540
  if #lst>0 then
68✔
541
    local ind_size = math.huge
68✔
542
    for i, line in ipairs(lst) do
272✔
543
      local i1, i2 = lst[i]:find('^%s*[^%s]')
204✔
544
      if i1 and i2 < ind_size then
204✔
545
        ind_size = i2
102✔
546
      end
547
    end
548
    for i, line in ipairs(lst) do
272✔
549
      lst[i] = lst[i]:sub(ind_size, -1)
260✔
550
    end
551
  end
552
  return concat(lst,'\n')..'\n'
68✔
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 = {}
1,530✔
562
    if #words[1] > size then
1,530✔
563
      -- word longer than line
564
      if not breaklong then
595✔
565
        line[1] = words[1]
221✔
566
        remove(words, 1)
286✔
567
      else
568
        line[1] = words[1]:sub(1, size)
484✔
569
        words[1] = words[1]:sub(size + 1, -1)
484✔
570
      end
571
    else
572
      local len = 0
935✔
573
      while words[1] and (len + #words[1] <= size) or
3,060✔
574
            (len == 0 and #words[1] == size) do
1,560✔
575
        if words[1] ~= "" then
2,125✔
576
          line[#line+1] = words[1]
1,955✔
577
          len = len + #words[1] + 1
1,955✔
578
        end
579
        remove(words, 1)
2,750✔
580
      end
581
    end
582
    return stringx.strip(concat(line, " ")), words
1,980✔
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
255✔
596
    s = stringx.strip(s) -- remove leading/trailing whitespace
326✔
597
    if s == "" then
255✔
598
      return { "" }
34✔
599
    end
600
    width = width or 70
221✔
601
    local out = {}
221✔
602
    local words = usplit(s, "%s")
221✔
603
    while words[1] do
1,751✔
604
      out[#out+1], words = buildline(words, width, breaklong)
1,980✔
605
    end
606
    return makelist(out)
221✔
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)
595✔
617
  return concat(stringx.wrap(s,width,breaklong),'\n') .. '\n'
22✔
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
66✔
627
    subst = tbl
×
628
  else
629
    function subst(f)
42✔
630
      local s = tbl[f]
68✔
631
      if not s then
68✔
632
        if safe then
×
633
          return f
×
634
        else
635
          error("not present in table "..f)
×
636
        end
637
      else
638
        return s
68✔
639
      end
640
    end
641
  end
642
  local res = gsub(s,'%${([%w_]+)}',subst)
51✔
643
  return (gsub(res,'%$([%w_]+)',subst))
51✔
644
end
645

646

647

648
local Template = {}
595✔
649
stringx.Template = Template
595✔
650
Template.__index = Template
595✔
651
setmetatable(Template, {
1,190✔
652
  __call = function(obj,tmpl)
653
    return Template.new(tmpl)
51✔
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)
595✔
663
  assert_arg(1,tmpl,'string')
51✔
664
  local res = {}
51✔
665
  res.tmpl = tmpl
51✔
666
  setmetatable(res,Template)
51✔
667
  return res
51✔
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)
595✔
675
  assert_arg(1,tbl,'table')
34✔
676
  return _substitute(self.tmpl,tbl,false)
34✔
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)
595✔
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)
595✔
696
  assert_arg(1,tbl,'table')
17✔
697
  if not self.strings then
17✔
698
    self.strings = usplit(self.tmpl,'\n')
22✔
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)
87✔
706
      local subtmpl
707
      local s = tbl[f]
17✔
708
      if not s then error("not present in table "..f) end
17✔
709
      if getmetatable(s) == Template then
17✔
710
        subtmpl = s
×
711
        s = s.tmpl
×
712
      else
713
        s = tostring(s)
17✔
714
      end
715
      if s:find '\n' then
17✔
716
        local lines = usplit(s, '\n')
17✔
717
        for i, line in ipairs(lines) do
68✔
718
          lines[i] = sp..line
51✔
719
        end
720
        s = concat(lines, '\n') .. '\n'
17✔
721
      end
722
      if subtmpl then
17✔
723
        return _substitute(s, tbl)
×
724
      else
725
        return s
17✔
726
      end
727
    end)
728
  end
729

730
  local lines = {}
17✔
731
  for i, line in ipairs(self.strings) do
68✔
732
    lines[i] = subst(line)
66✔
733
  end
734
  return concat(lines,'\n')..'\n'
17✔
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)
595✔
752
    assert_string(1,s)
51✔
753
    if not s:find '\n$' then s = s..'\n' end
51✔
754
    return s:gmatch('([^\n]*)\n')
51✔
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)
595✔
763
    assert_string(1,s)
51✔
764
    return (s:gsub('(%S)(%S*)',function(f,r)
102✔
765
        return f:upper()..r:lower()
108✔
766
    end))
767
end
768

769
stringx.capitalize = stringx.title
595✔
770

771
do
772
  local ellipsis = '...'
595✔
773
  local n_ellipsis = #ellipsis
595✔
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)
595✔
784
      assert_string(1,s)
306✔
785
      if #s > w then
306✔
786
          if w < n_ellipsis then return ellipsis:sub(1,w) end
187✔
787
          if tail then
85✔
788
              local i = #s - w + 1 + n_ellipsis
34✔
789
              return ellipsis .. s:sub(i)
42✔
790
          else
791
              return s:sub(1,w-n_ellipsis) .. ellipsis
62✔
792
          end
793
      end
794
      return s
119✔
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'
809✔
807
      local equals, new_equals, _
808
      local finish = 1
809✔
809
      repeat
810
          _, finish, _, new_equals = s:find(lstring_pat, finish)
1,642✔
811
          if new_equals then
1,642✔
812
              equals = max(equals or 0, #new_equals)
833✔
813
          end
814
      until not new_equals
1,642✔
815

816
      return equals
809✔
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)
595✔
823
      assert_string(1,s)
809✔
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 .. "]")
809✔
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
809✔
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)
289✔
835
          -- Long strings strip out leading newline. We want to retain that, when quoting.
836
          if s:find("^\n") then s = "\n" .. s end
289✔
837
          local lbracket, rbracket =
838
              "[" .. equal_signs .. "[",
289✔
839
              "]" .. equal_signs .. "]"
289✔
840
          s = lbracket .. s .. rbracket
289✔
841
      else
842
          -- Escape funny stuff. Lua 5.1 does not handle "\r" correctly.
843
          s = ("%q"):format(s):gsub("\r", "\\r")
520✔
844
      end
845
      return s
809✔
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()
595✔
874

875
  local format = string.format
17✔
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(...)
51✔
881
    local i = 1
51✔
882
    for p in fmt:gmatch('%%.') do
119✔
883
      if p == '%s' and type(args[i]) ~= 'string' then
68✔
884
        args[i] = tostring(args[i])
22✔
885
      end
886
      i = i + 1
68✔
887
    end
888
    return format(fmt,unpack(args))
66✔
889
  end
890

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

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

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

917
return stringx
595✔
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

© 2026 Coveralls, Inc