Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/resources/pandoc/datadir/lpegfenceddiv.lua
12922 views
1
-- LPEG "parsing" and code for fenced div workarounds
2
-- Copyright (C) 2024 Posit Software, PBC
3
4
local lpeg = require('lpeg')
5
local colons = lpeg.P(':')^3
6
local maybe_spaces = lpeg.S("\t ")^0
7
local newline = lpeg.P("\n")
8
9
local single_quoted_string = lpeg.C(lpeg.P("'") * (lpeg.P("\\'") + (lpeg.P(1) - lpeg.P("'")))^0 * lpeg.P("'"))
10
local double_quoted_string = lpeg.C(lpeg.P('"') * (lpeg.P('\\"') + (lpeg.P(1) - lpeg.P('"')))^0 * lpeg.P('"'))
11
local literal = lpeg.C(
12
(lpeg.R("az", "AZ") + lpeg.S("_#.=")) *
13
(lpeg.R("az", "AZ", "09") + lpeg.S(".=-_"))^0
14
)
15
local Cp = lpeg.Cp()
16
17
local function anywhere(p)
18
return lpeg.P{ p + 1 * lpeg.V(1) }
19
end
20
local function anywhere_pos(p)
21
return lpeg.P{ Cp * p * Cp + 1 * lpeg.V(1) }
22
end
23
24
local div_attr_block = lpeg.P("{") * maybe_spaces * ((single_quoted_string + double_quoted_string + literal) * maybe_spaces)^0 * lpeg.P("}")
25
26
local start_div = colons * maybe_spaces * div_attr_block * (newline + lpeg.P(-1))
27
local start_div_search = anywhere_pos(start_div)
28
29
local function first_and_last(...)
30
local arg = {...}
31
local n = #arg
32
return arg[1], arg[n]
33
end
34
35
local single_quote_p = anywhere(lpeg.P("'"))
36
local double_quote_p = anywhere(lpeg.P('"'))
37
local bad_equals = anywhere_pos(lpeg.P("= ") + (lpeg.P(" =") * lpeg.P(" ")^-1))
38
39
local function attempt_to_fix_fenced_div(txt)
40
local b, e = first_and_last(start_div_search:match(txt))
41
while b do
42
local substring = txt:sub(b, e - 1)
43
local function count(txt, p, b)
44
local result = 0
45
if not b then
46
b = 1
47
end
48
while b do
49
b = p:match(txt, b)
50
if b then
51
result = result + 1
52
end
53
end
54
return result
55
end
56
-- now we try to find the dangerous `=` with spaces around it
57
-- the best heuristic we have at the moment is to look for a ` = `, `= ` or ` =`
58
-- and then attempt to rule out that the `=` is part of a quoted string
59
-- if `=` is not part of a quoted string, then we'll have an even number of single and double quotes
60
-- to the left and right of the `=`
61
-- if there's a total odd number of quotes, then this is a badly formatted key-value pair
62
-- for a _different_ reason, so we do nothing
63
64
local bad_eq, bad_eq_end = bad_equals:match(substring)
65
if bad_eq then
66
local total_single = count(substring, single_quote_p)
67
local total_double = count(substring, double_quote_p)
68
local right_single = count(substring, single_quote_p, bad_eq_end)
69
local right_double = count(substring, double_quote_p, bad_eq_end)
70
local left_single = total_single - right_single
71
local left_double = total_double - right_double
72
if left_single % 2 == 0 and right_single % 2 == 0 and left_double % 2 == 0 and right_double % 2 == 0 then
73
-- we have a bad key-value pair
74
-- we need to replace the `=` with _no spaces_
75
local replacement = substring:sub(1, bad_eq - 1) .. "=" .. substring:sub(bad_eq_end)
76
local pad_length = #replacement - #substring
77
78
-- in order to keep the string length the same, we need add spaces to the end of the block
79
txt = txt:sub(1, b - 1) .. replacement .. txt:sub(e) .. (" "):rep(pad_length)
80
81
-- if substitution was made, we need to search at the beginning again
82
-- to find the next bad key-value pair in the same block
83
b, e = first_and_last(start_div_search:match(txt, b))
84
else
85
b, e = first_and_last(start_div_search:match(txt, e))
86
end
87
else
88
b, e = first_and_last(start_div_search:match(txt, e))
89
end
90
end
91
return txt
92
end
93
94
---------------------------------------------------
95
96
local div_attr_block_tests = {
97
"{#id .class key='value'}",
98
"{#id .class key=value}",
99
'{#id .class key="value with spaces"}',
100
}
101
102
local div_block_tests = {
103
"::: {#id .class key='value'}",
104
"::: {#id .class key=value}",
105
'::: {#id .class key="value with spaces"}',
106
}
107
local end_to_end_tests = {
108
"::: {#id-1 .class key =value}\nfoo\n:::\n\n::: {#id-2 .class key='value'}\nfoo\n:::\n",
109
"::: {#id-1 .class key = value}\nfoo\n:::\n\n::: {#id-2 .class key='value'}\nfoo\n:::\n",
110
"::: {#id-1 .class key= value}\nfoo\n:::\n\n::: {#id-2 .class key='value'}\nfoo\n:::\n",
111
"::: {#id-1 .class key =value}\nfoo\n:::\n\n::: {#id-2 .class key= 'value'}\nfoo\n:::\n",
112
"::: {#id-1 .class key = value}\nfoo\n:::\n\n::: {#id-2 .class key = 'value'}\nfoo\n:::\n",
113
"::: {#id-1 .class key= value}\nfoo\n:::\n\n::: {#id-2 .class key ='value'}\nfoo\n:::\n",
114
"::: {#id-1 .class key= value please='do not touch = this one'}\nfoo\n:::",
115
"::: {#id-1 .class key= value key2 =value2}\nfoo\n:::",
116
"::: {#id-4 key = value}\nfoo\n:::",
117
}
118
119
local function tests()
120
for _, test in ipairs(div_attr_block_tests) do
121
print(div_attr_block:match(test))
122
end
123
for _, test in ipairs(div_block_tests) do
124
print(start_div_search:match(test))
125
end
126
for _, test in ipairs(end_to_end_tests) do
127
print(attempt_to_fix_fenced_div(test))
128
print("---")
129
end
130
end
131
132
return {
133
_tests = tests,
134
attempt_to_fix_fenced_div = attempt_to_fix_fenced_div
135
}
136