Be a bit more lenient on whether * is included in controlname
[llf.git] / llf.lua
blobfe4a794799420a7d63fcc16180f93ea3589cb272
1 #!/usr/bin/env lua
3 local lpeg = require('lpeg')
4 local wcwidth = require('wcwidth')
5 local utf8 = require('utf8')
7 local V, R, P, S, C, Ct, Cg, Cmt, Cb =
8 lpeg.V, lpeg.R, lpeg.P, lpeg.S, lpeg.C, lpeg.Ct, lpeg.Cg, lpeg.Cmt,
9 lpeg.Cb
11 -- Configuration
13 function to_set (l)
14 if not l then return {} end
15 s = {}
16 for _, v in ipairs(l) do s[v] = true end
17 return s
18 end
20 function load_conf (p) pcall(function () conf = dofile(p) end) end
22 conf = nil
24 load_conf('/usr/share/llf/llfrc.lua')
26 if os.getenv('HOME') then
27 load_conf(os.getenv('HOME')..'/.config/llf/llfrc.lua')
28 end
30 if os.getenv('XDG_CONFIG_HOME') then
31 load_conf(os.getenv('XDG_CONFIG_HOME')..'/llf/llfrc.lua')
32 end
34 if #arg == 0 then
35 elseif #arg == 2 and arg[1] == '-c' then
36 if pcall(function () conf = dofile(arg[2]) end) then
37 else
38 io.stderr:write("couldn't load config file "..arg[2].."\n")
39 os.exit(1)
40 end
41 else
42 io.write('Usage: '..arg[0]..' [ -c /path/to/config/file ]\n')
43 os.exit(0)
44 end
46 if not conf then
47 io.stderr:write("config file appears malformed\n")
48 os.exit(1)
49 end
51 conf.inline_environments = to_set(conf.inline_environments)
52 conf.own_line_controlseqs = to_set(conf.own_line_controlseqs)
53 conf.start_line_controlseqs = to_set(conf.start_line_controlseqs)
54 conf.tabular_like_environments = to_set(conf.tabular_like_environments)
55 conf.own_paragraphs = to_set(conf.own_paragraphs)
56 conf.indent_incrementers = to_set(conf.indent_incrementers)
57 conf.indent_decrementers = to_set(conf.indent_decrementers)
58 conf.max_len = math.max(conf.max_len, 2)
60 -- build grammar
61 function f (s, i, delim, c) return delim == c end
63 constructed_verbatims = (Cg("\\verb", "beginbit") *
64 Cg(V"verbposdelims", "delim") *
65 Cg((P(1) - Cmt(Cb("delim") * C(1), f))^0, "content") *
66 Cg(Cmt(Cb("delim") * C(1), f), "endbit"))
67 for _, n in ipairs(conf.verbatim_like_environments) do
68 constructed_verbatims = constructed_verbatims +
69 (Cg("\\begin{"..n.."}", "beginbit") *
70 Cg((P(1) - #P("\\end{"..n.."}"))^0, "content") *
71 Cg("\\end{"..n.."}", "endbit"))
72 end
74 texish = P{
75 "document";
76 ws = S(" \t\v\n"),
77 comment = (P"%" * (P(1) - P"\n")^0),
78 controlname = C((P"left" + P"right") *
79 ((P(1) - P"\\") + (V"controlseq"))) +
80 (R"az" + R"AZ" + R"09" + "@")^1 * P"*" *
81 #(1 - (R"az" + R"AZ" + R"09" + S"@*")) +
82 (R"az" + R"AZ" + R"09" + "@")^1 *
83 #(1 - (R"az" + R"AZ" + R"09")),
84 controlseq = Ct(P"\\" * Cg(V"controlname", "name") *
85 Cg(Ct((V"subbody")^0), "args")),
86 verbposdelims = P(P(1) - P"*"),
87 verbdelim = C(V"verbposdelims"),
88 verbatimbody = Ct((Cg("%noformat{\n", "beginbit") *
89 Cg((P(1) - #P"%}noformat\n")^0, "content") *
90 Cg("%}noformat", "endbit") * #P"\n") +
91 constructed_verbatims),
92 specialcontrol = Ct(P"\\" * Cg(S"\\ \n%,:;\"\'-{}$^_`~#&[]()!", "name")),
93 word = S"[]" + (1 - S(" \t\n\v\\%{}[]"))^1,
94 wordnb = (1 - S(" \t\n\v\\%{}[]"))^1,
95 subbody = Ct(Cg(P"{", "l") * Cg(V"body", "body") * Cg(P"}", "r")) +
96 Ct(Cg(P"[", "l") * Cg(V"bodynb", "body") * Cg(P"]", "r")),
97 subbodynb = Ct(Cg(P"{", "l") * Cg(V"body", "body") * Cg(P"}", "r")),
98 body = Ct(Ct(Cg(V"verbatimbody", "verbatim") +
99 Cg(V"comment", "comment") +
100 Cg(V"ws"^1, "whitespace") +
101 Cg(V"subbody", "subbody") +
102 Cg(V"specialcontrol", "controlseq") +
103 Cg(V"controlseq", "controlseq") +
104 Cg(V"word", "word"))^0),
105 bodynb = Ct(Ct(Cg(V"verbatimbody", "verbatim") +
106 Cg(V"comment", "comment") +
107 Cg(V"ws"^1, "whitespace") +
108 Cg(V"subbody", "subbody") +
109 Cg(V"specialcontrol", "controlseq") +
110 Cg(V"controlseq", "controlseq") +
111 Cg(V"wordnb", "word"))^0),
112 document = V"body" * (-1)
115 -- state
116 prog_state = {
117 indent_level = 0,
118 cell_pos = 0,
119 env_depth = 0,
120 tabular_depth = 0,
121 printed_anything = false,
122 sp_before_next = false,
123 nl_before_next = 0,
124 next_chunk = "",
125 next_chunk_indent = 0,
128 -- put enough spaces to start a new line at proper indentation
129 function indent_to(state)
130 i = state.indent_level
131 if state.next_chunk ~= "" then
132 i = state.next_chunk_indent
134 l = (i * 2) % conf.max_len
135 io.write(string.rep(" ", l))
137 state.cell_pos = l
138 state.sp_before_next = false
141 -- calculate (by wcwidth()) cells needed to display a string
142 function display_width(s)
143 local len = 0
144 for _, rune in utf8.codes(s) do
145 local l = wcwidth(rune)
146 if l >= 0 then
147 len = len + l
150 return len
153 -- handle seeing \begin{something}
154 function begin_env(state, e)
155 state.env_depth = state.env_depth + 1
156 state.indent_level = state.indent_level + 1
157 if conf.tabular_like_environments[e] and
158 state.tabular_depth == 0 then
159 state.tabular_depth = state.env_depth
161 if not conf.inline_environments[e] then
162 state.nl_before_next = math.max(state.nl_before_next, 1)
166 -- handle seeing \end{something}
167 function end_env(state, e)
168 state.env_depth = state.env_depth - 1
169 if state.tabular_depth > state.env_depth then
170 state.tabular_depth = 0
172 state.indent_level = state.indent_level - 1
173 if not conf.inline_environments[e] then
174 state.nl_before_next = math.max(state.nl_before_next, 1)
178 function indent_check(state, c)
179 if conf.indent_incrementers[c.name] then
180 flush_chunk(state)
181 state.indent_level = state.indent_level + 1
185 function deindent_check(state, c)
186 if conf.indent_decrementers[c.name] then
187 flush_chunk(state)
188 state.indent_level = state.indent_level - 1
192 -- handle some control sequences starting their own lines
193 function ensure_start_line(state, c)
194 if conf.start_line_controlseqs[c.name] then
195 flush_chunk(state)
196 state.nl_before_next = math.max(state.nl_before_next, 1)
200 -- handle some control sequences being on their own lines
201 function ensure_own_line(state, c)
202 if c.name == "begin" or c.name == "end" then
203 if c.args and c.args[1] and c.args[1].body and
204 c.args[1].body and c.args[1].body[1] then
205 e = c.args[1].body[1].word
206 if type(e) == "string" and
207 conf.inline_environments[e] then
208 return
211 flush_chunk(state)
212 state.nl_before_next = math.max(state.nl_before_next, 1)
215 if conf.own_line_controlseqs[c.name] then
216 flush_chunk(state)
217 state.nl_before_next = math.max(state.nl_before_next, 1)
220 if conf.own_paragraphs[c.name] then
221 flush_chunk(state)
222 state.nl_before_next = math.max(state.nl_before_next, 2)
226 -- print something to the screen, properly indented
227 function write_out_string(state, s)
228 if state.nl_before_next > 0 then
229 if state.printed_anything then
230 io.write(string.rep("\n", state.nl_before_next))
231 indent_to(state)
233 state.sp_before_next = false
236 spacelen = (state.sp_before_next and 1) or 0
238 slen = display_width(s)
240 projected_cell_pos = state.cell_pos + spacelen + slen
241 if_newlined = ((state.indent_level * 2) % conf.max_len) + spacelen +
242 slen
243 if (projected_cell_pos > conf.max_len) and
244 (if_newlined <= conf.max_len) and
245 (state.tabular_depth == 0) then
246 io.write("\n")
247 cell_pos = 0
248 spacelen = 0
249 indent_to(state)
252 if spacelen > 0 then
253 io.write(" ")
254 state.cell_pos = state.cell_pos + 1
257 io.write(s)
258 state.printed_anything = true
259 state.cell_pos = state.cell_pos + slen
260 state.nl_before_next = 0
261 state.sp_before_next = false
264 -- commit a chunk of text to be displayed
265 function commit_string(state, s)
266 if state.next_chunk == "" then
267 state.next_chunk_indent = state.indent_level
269 state.next_chunk = state.next_chunk..s
272 -- flush out the last chunk completely
273 function flush_chunk(state)
274 if state.next_chunk == "" then return end
275 write_out_string(state, state.next_chunk)
276 state.next_chunk = ""
279 -- handle whitespace
280 function p_whitespace(state, w)
281 flush_chunk(state)
282 nls = 0
283 for i = 1, string.len(w), 1 do
284 if string.sub(w, i, i) == "\n" then
285 nls = nls + 1
286 else
287 state.sp_before_next = true
291 if nls > 0 and state.tabular_depth > 0 then
292 state.nl_before_next = math.max(state.nl_before_next, 1)
293 elseif nls == 1 then
294 state.sp_before_next = true
295 elseif nls >= 2 then
296 state.nl_before_next = math.max(state.nl_before_next, 2)
300 -- print out a control sequence, and handle indentation
301 function p_controlseq(state, c)
302 deindent_check(state, c)
303 if c.name == "end" and c.args and c.args[1] and
304 c.args[1].body and c.args[1].body[1] and
305 c.args[1].body[1].word then
306 flush_chunk(state)
307 end_env(state, c.args[1].body[1].word)
310 ensure_start_line(state, c)
311 ensure_own_line(state, c)
313 if c.name == "\n" then
314 commit_string(state, "\\")
315 flush_chunk(state)
316 state.nl_before_next = math.max(state.nl_before_next, 1)
317 return
320 commit_string(state, "\\"..c.name)
321 if not c.args then c.args = {} end
322 for _, a in ipairs(c.args) do
323 p_subbody(state, a)
326 indent_check(state, c)
327 ensure_own_line(state, c)
328 if c.name == "begin" and c.args and c.args[1] and
329 c.args[1].body and c.args[1].body[1] and
330 c.args[1].body[1].word then
331 begin_env(state, c.args[1].body[1].word)
335 -- print out a body that was enclosed by { and }
336 function p_subbody(state, b)
337 commit_string(state, b.l)
338 state.indent_level = state.indent_level + 1
339 p_body(state, b.body)
340 state.indent_level = state.indent_level - 1
341 commit_string(state, b.r)
344 -- a verbatim environment
345 function p_verbatim(state, v)
346 -- XXX: fix the grammar that caused this mess
347 control = nil
348 comment = false
350 if v.beginbit == "\\begin{verbatim}" then
351 control = {name="begin", body="verbatim"}
352 elseif v.beginbit == "\\begin{Verbatim}" then
353 control = {name="begin", body="Verbatim"}
354 elseif v.beginbit == "\\begin{alltt}" then
355 control = {name="begin", body="alltt"}
356 elseif v.beginbit == "\\verb" then
357 control = {name="verb"}
358 elseif string.sub(v.beginbit, 1, 1) == "%" then
359 comment = true
362 if control then ensure_own_line(state, control) end
364 first_nl_i, first_nl_j = string.find(v.content, "\n")
366 s = v.beginbit
367 if v.delim then s = s..v.delim end
368 if first_nl_i then
369 s = s..string.sub(v.content, 1, first_nl_i - 1)
370 v.content = string.sub(v.content, first_nl_i, -1)
371 else
372 s = s..v.content
373 v.content = ""
375 commit_string(state, s)
376 flush_chunk(state)
377 io.write(v.content)
378 io.write(v.endbit)
380 if comment then
381 state.nl_before_next = math.max(state.nl_before_next, 1)
384 rest = v.content..v.endbit
385 last_nl = -1
386 while true do
387 i = string.find(rest, "\n", last_nl + 1)
388 if i == nil then break end
389 last_nl = i
392 last_line_len = display_width(string.sub(rest, last_nl + 1, -1))
393 if last_nl >= 0 then
394 state.cell_pos = last_line_len
395 else
396 state.cell_pos = state.cell_pos + last_line_len
399 if control then
400 ensure_own_line(state, control)
404 -- print out a comment
405 function p_comment(state, c)
406 flush_chunk(state)
407 write_out_string(state, c)
408 state.nl_before_next = math.max(state.nl_before_next, 1)
411 -- print out a document contents
412 function p_body (state, b)
413 if b == nil or b == "" then return end
414 for _, bb in ipairs(b) do
415 if bb.comment then p_comment(state, bb.comment) end
416 if bb.whitespace then p_whitespace(state, bb.whitespace) end
417 if bb.controlseq then p_controlseq(state, bb.controlseq) end
418 if bb.word then commit_string(state, bb.word) end
419 if bb.verbatim then p_verbatim(state, bb.verbatim) end
420 if bb.subbody then p_subbody(state, bb.subbody) end
424 -- slurp input
425 corpus = ""
427 while true do
428 local line = io.read()
429 if line == nil then break end
430 if corpus == "" then
431 corpus = line
432 else
433 corpus = corpus .. "\n" .. line
438 -- parse, perhaps print
439 local m = texish:match(corpus)
440 if not m then
441 io.write(corpus)
442 io.write("\n")
443 io.stderr:write("llf: cannot parse input\n")
444 os.exit(1)
445 else
446 p_body(prog_state, m)
447 flush_chunk(prog_state)
448 io.write("\n")