├── deno
├── README.pod
├── podium.lua
├── app.ts
└── index.html
├── stylua.toml
├── .gitpod.Dockerfile
├── .luarc.json
├── .vscode
└── settings.json
├── .gitpod.yml
├── Makefile
├── README.pod
├── doc
└── podium.txt
├── spec
└── podium_spec.lua
└── lua
└── podium.lua
/deno/README.pod:
--------------------------------------------------------------------------------
1 | ../README.pod
--------------------------------------------------------------------------------
/deno/podium.lua:
--------------------------------------------------------------------------------
1 | ../lua/podium.lua
--------------------------------------------------------------------------------
/stylua.toml:
--------------------------------------------------------------------------------
1 | indent_type = "Spaces"
2 | indent_width = 2
3 | column_width = 80
4 |
--------------------------------------------------------------------------------
/.gitpod.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM gitpod/workspace-full
2 | RUN brew install lua busted stylua deno
--------------------------------------------------------------------------------
/.luarc.json:
--------------------------------------------------------------------------------
1 | {
2 | "workspace.ignoreDir": [
3 | ".vscode",
4 | ".git",
5 | "deno"
6 | ],
7 | "diagnostics.globals": [
8 | "it",
9 | "describe"
10 | ],
11 | "hint.enable": true
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.inlayHints.enabled": "on",
3 | "deno.enable": true,
4 | "deno.inlayHints.functionLikeReturnTypes.enabled": true,
5 | "deno.inlayHints.variableTypes.enabled": true,
6 | "deno.inlayHints.parameterTypes.enabled": true
7 | }
8 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 |
2 | tasks:
3 | - name: Launch server
4 | command: make serve
5 |
6 | vscode:
7 | extensions:
8 | - sumneko.lua
9 | - denoland.vscode-deno
10 |
11 | ports:
12 | - port: 8000
13 | visibility: private
14 |
15 | image:
16 | file: .gitpod.Dockerfile
17 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: format test serve doc
2 |
3 | format:
4 | stylua **/*.lua
5 |
6 | doc:
7 | ./lua/podium.lua vimdoc README.pod > doc/podium.txt
8 |
9 | check:
10 | lua-language-server --check lua/podium.lua
11 |
12 | test:
13 | busted -m lua/?.lua
14 |
15 | serve:
16 | deno run -A deno/app.ts
17 |
--------------------------------------------------------------------------------
/deno/app.ts:
--------------------------------------------------------------------------------
1 | import { Hono } from "https://lib.deno.dev/x/hono@v3/mod.ts";
2 | import { serveStatic } from "https://lib.deno.dev/x/hono@v3/middleware.ts";
3 | import { serve } from "https://deno.land/std@0.191.0/http/server.ts";
4 | import { LuaFactory } from "https://jspm.dev/wasmoon@1.15.0"
5 |
6 | const rawCode = await fetch(new URL("./podium.lua", import.meta.url))
7 | .then(r => r.text())
8 | .then(r => r.replace("#!/usr/bin/env lua", ""))
9 | const lua = await (new LuaFactory('https://unpkg.com/wasmoon@1.15.0/dist/glue.wasm')).createEngine()
10 | const podium = await lua.doString(rawCode)
11 |
12 | const app = new Hono();
13 |
14 | app.post("/:backend{html|markdown|latex|vimdoc}", async (ctx) => {
15 | const source = await ctx.req.text();
16 | const backend = ctx.req.param("backend");
17 | const result = await podium.process(backend, source);
18 | return ctx.text(result);
19 | });
20 |
21 | const { pathname } = new URL(import.meta.url);
22 | const dirname = pathname.substring(0, pathname.lastIndexOf("/"));
23 | const root = dirname.replace(Deno.cwd(), "");
24 | app.get("/*", serveStatic({ root }));
25 |
26 | serve(app.fetch);
27 |
--------------------------------------------------------------------------------
/deno/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Podium Playground
6 |
7 |
8 |
42 |
54 |
55 |
56 | POD Processor Podium : Playground
57 |
58 |
59 |
60 |
61 | HTML
62 | HTML (code)
63 | Vimdoc
64 | LaTeX
65 | Markdown
66 |
67 |
68 |
69 | Convert
70 |
71 |
72 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/README.pod:
--------------------------------------------------------------------------------
1 | ---
2 | name: podium
3 | description: POD parser and tool
4 | ---
5 |
6 | =pod
7 |
8 | =head1 Podium
9 |
10 | =head2 Playground X
11 |
12 | You can try Podium in your browser at L .
13 |
14 | =head2 Description X
15 |
16 | This is a parser and tool for L.
17 |
18 | =head2 Features X
19 |
20 | POD parser provides a convenient way to write documentation and comes with the
21 | following features:
22 |
23 | =over
24 |
25 | =item * Easy-to-read syntax
26 |
27 | =item * Multiple output formats (HTML, Markdown, LaTeX, Vimdoc)
28 |
29 | =item * Command line interface for simple conversion
30 |
31 | =item * Extensible for integration into other projects
32 |
33 | =back
34 |
35 | To get started using POD, download a file and follow
36 | the usage instructions provided in the subsequent sections.
37 |
38 | =head2 Installation X
39 |
40 | $ wget https://pod.deno.dev/podium.lua
41 | $ chmod +x podium.lua
42 |
43 | =head2 Usage X
44 |
45 | To use Podium, you can either use the WebAPI, the command line interface, or
46 | the application programming interface.
47 |
48 | =head3 WebAPI X
49 |
50 | WebAPI is available at L .
51 |
52 | $ curl --data-binary @path/to/file.pod https://pod.deno.dev/markdown
53 | $ curl --data-binary `$(cat path/to/file.pod)` https://pod.deno.dev/html
54 | $ cat path/to/file.pod | curl --data-binary @- https://pod.deno.dev/latex
55 |
56 | =head3 Command Line Interface X
57 |
58 | To run the command line interface, you need to install Lua.
59 |
60 | $ podium.lua markdown path/to/file.pod path/to/file.md # write markdown
61 | $ podium.lua latex path/to/file.pod path/to/file.tex # write latex
62 | $ podium.lua vimdoc path/to/file.pod path/to/file.txt # write vimdoc
63 | $ podium.lua html path/to/file.pod path/to/file.html # write html
64 |
65 | $ podium.lua html path/to/file.pod > path/to/file.html # wirte html to stdout
66 | $ podium.lua html < path/to/file.pod > path/to/file.html # write html to stdout, read pod from stdin
67 |
68 | =head3 Application Programming Interface X
69 |
70 | If you want to use Podium in your own project, you can use the application
71 | programming interface (API) to convert POD to HTML, Markdown, LaTeX, or Vimdoc.
72 |
73 | local podium = require('podium')
74 | local inputString = "..."
75 | local backend = podium.html -- or markdown, latex, vimdoc
76 | print(podium.process(backend, inputString)) -- process returns output string
77 |
78 | =over
79 |
80 | =item 1. Create a new C object,
81 | which takes an output format as an argument.
82 |
83 | =item 2. Call the C method on the PodiumProcessor object.
84 |
85 | =back
86 |
87 | To customize the output, see below.
88 |
89 | =head2 Customization X
90 |
91 | B
92 |
93 | -- example.lua
94 | podium = dofile('podium.lua')
95 |
96 | -- customize the output
97 | podium.html:registerSimpleFormattingCode('B', function (text)
98 | return '' .. text .. ' '
99 | end)
100 |
101 | -- read file as string
102 | local inputString = io.open('path/to/file.pod'):read('*a')
103 |
104 | -- process the string
105 | local outputString = podium.process(podium.html, inputString)
106 |
107 | -- write the string to file
108 | io.open('path/to/file.html', 'w'):write(outputString)
109 |
110 | Podium consists of the three components:
111 |
112 | =over
113 |
114 | =item * C converts a string into another string using a C instance.
115 |
116 | =item * C converts the tree structure into a string.
117 |
118 | =item * C represents a node in the tree structure.
119 |
120 | =back
121 |
122 | Please be relaxed, you don't need to know the details of the tree structure.
123 | You just need to arrange the simple string-to-string conversion.
124 |
125 | You can customize the output by tweaking C instance,
126 | which has four methods for simple customization:
127 |
128 | =head3 C X
129 |
130 | This method registers a simple formatting code, e.g., C...E>.
131 | C is the name of the formatting code: the single capital letter.
132 | C is a function that takes a string and returns a string.
133 |
134 | local podium = require('podium')
135 | local inputString = "..."
136 | local backend = podium.html -- or markdown, latex, vimdoc
137 | backend:registerSimpleFormattingCode('B', function (text)
138 | return '' .. text .. ' '
139 | end)
140 | print(podium.process(backend, inputString)) -- process returns output string
141 |
142 | =head3 C X
143 |
144 | This method registers a simple command, e.g., C<=head1 ...>.
145 | C is the name of the command defined in the POD document.
146 | Please do not override C<=begin> and C<=end> commands.
147 |
148 | local podium = require('podium')
149 | local inputString = "..."
150 | local backend = podium.html -- or markdown, latex, vimdoc
151 | backend:registerSimpleCommand('head1', function (text)
152 | return '' .. text .. ' '
153 | end)
154 | print(podium.process(backend, inputString)) -- process returns output string
155 |
156 | =head3 C X
157 |
158 | This method registers a simple data paragraph, e.g., content between
159 | C<=begin name> and C<=end name> commands.
160 | C is the name of the data paragraph, e.g., html or markdown.
161 |
162 | local podium = require('podium')
163 | local inputString = "..."
164 | local backend = podium.html -- or markdown, latex, vimdoc
165 | backend:registerSimpleDataParagraph('html', function (text)
166 | return '' .. text .. '
'
167 | end)
168 | print(podium.process(backend, inputString)) -- process returns output string
169 |
170 | =head3 C X
171 |
172 | This method is used to register another simple conversion, e.g., preamble, and
173 | postambles. C is the name of the conversion, e.g., preamble, and
174 | postambles. C is a function that takes a string and returns a string.
175 | The function takes the entire input string as an argument for preamble and
176 | postambles.
177 |
178 | local podium = require('podium')
179 | local inputString = "..."
180 | local backend = podium.html -- or markdown, latex, vimdoc
181 | backend
182 | :registerSimple('preamble', function (text)
183 | return ''
184 | end)
185 | :registerSimple('postamble', function (text)
186 | return ''
187 | end)
188 | print(podium.process(html, inputString)) -- process returns output string
189 |
190 | =head2 JavaScript API X
191 |
192 | Podium is written in Lua, but you can use it in JavaScript as well.
193 |
194 | import {LuaFactory} from 'npm:wasmoon@latest';
195 | const luaFactory = new LuaFactory();
196 | const lua = await luaFactory.createEngine();
197 | const code = Deno.readTextFileSync('./lua/podium.lua').replace("#!/usr/bin/env lua", "")
198 | const pod = await lua.doString(code);
199 |
200 | // Arguments: backend name, node name, modifier function
201 | pod.PodiumBackend.registerSimpleDataParagraph("html", "red", (arg) => {
202 | return `${red}`
203 | })
204 |
205 | // Arguments: backend name, input string
206 | // Returns: output string
207 | pod.process("html", "...")
208 |
209 | =head2 License X
210 |
211 | Licensed under MIT License.
212 |
213 | Copyright (c) 2022 TANIGUCHI Masaya
214 |
215 | =cut
216 |
--------------------------------------------------------------------------------
/doc/podium.txt:
--------------------------------------------------------------------------------
1 | *podium.txt* POD parser and tool
2 | =============================================================================
3 | Podium~
4 |
5 | Playground ~
6 | *podium-playground*
7 |
8 | You can try Podium in your browser at |https://pod.deno.dev/|.
9 |
10 | Description ~
11 | *podium-description*
12 |
13 | This is a parser and tool for Plain Old Documentation (POD) |https://perldoc.perl.org/perlpod|.
14 |
15 | Features ~
16 | *podium-features*
17 |
18 | POD parser provides a convenient way to write documentation and comes with the
19 | following features:
20 |
21 | - Easy-to-read syntax
22 | - Multiple output formats (HTML, Markdown, LaTeX, Vimdoc)
23 | - Command line interface for simple conversion
24 | - Extensible for integration into other projects
25 |
26 | To get started using POD, download a file and follow
27 | the usage instructions provided in the subsequent sections.
28 |
29 | Installation ~
30 | *podium-installation*
31 |
32 | >
33 | $ wget https://pod.deno.dev/podium.lua
34 | $ chmod +x podium.lua
35 | <
36 |
37 | Usage ~
38 | *podium-usage*
39 |
40 | To use Podium, you can either use the WebAPI, the command line interface, or
41 | the application programming interface.
42 |
43 | WebAPI ~
44 | *podium-webapi*
45 |
46 | WebAPI is available at |https://pod.deno.dev/|.
47 |
48 | >
49 | $ curl --data-binary @path/to/file.pod https://pod.deno.dev/markdown
50 | $ curl --data-binary `$(cat path/to/file.pod)` https://pod.deno.dev/html
51 | $ cat path/to/file.pod | curl --data-binary @- https://pod.deno.dev/latex
52 | <
53 |
54 | Command Line Interface ~
55 | *podium-cli*
56 |
57 | To run the command line interface, you need to install Lua.
58 |
59 | >
60 | $ podium.lua markdown path/to/file.pod path/to/file.md # write markdown
61 | $ podium.lua latex path/to/file.pod path/to/file.tex # write latex
62 | $ podium.lua vimdoc path/to/file.pod path/to/file.txt # write vimdoc
63 | $ podium.lua html path/to/file.pod path/to/file.html # write html
64 |
65 | $ podium.lua html path/to/file.pod > path/to/file.html # wirte html to stdout
66 | $ podium.lua html < path/to/file.pod > path/to/file.html # write html to stdout, read pod from stdin
67 | <
68 |
69 | Application Programming Interface ~
70 | *podium-api*
71 |
72 | If you want to use Podium in your own project, you can use the application
73 | programming interface (API) to convert POD to HTML, Markdown, LaTeX, or Vimdoc.
74 |
75 | >
76 | local podium = require('podium')
77 | local inputString = "..."
78 | local backend = podium.html -- or markdown, latex, vimdoc
79 | print(podium.process(backend, inputString)) -- process returns output string
80 | <
81 |
82 | - Create a new `PodiumProcessor` object,
83 | which takes an output format as an argument.
84 | - Call the `process` method on the PodiumProcessor object.
85 |
86 | To customize the output, see below.
87 |
88 | Customization ~
89 | *podium-customization*
90 |
91 | {Example:}
92 |
93 | >
94 | -- example.lua
95 | podium = dofile('podium.lua')
96 |
97 | -- customize the output
98 | podium.html:registerSimpleFormattingCode('B', function (text)
99 | return '' .. text .. ' '
100 | end)
101 |
102 | -- read file as string
103 | local inputString = io.open('path/to/file.pod'):read('*a')
104 |
105 | -- process the string
106 | local outputString = podium.process(podium.html, inputString)
107 |
108 | -- write the string to file
109 | io.open('path/to/file.html', 'w'):write(outputString)
110 | <
111 |
112 | Podium consists of the three components:
113 |
114 | - `process` converts a string into another string using a `PodiumBackend` instance.
115 | - `PodiumBackend` converts the tree structure into a string.
116 | - `PodiumElement` represents a node in the tree structure.
117 |
118 | Please be relaxed, you don't need to know the details of the tree structure.
119 | You just need to arrange the simple string-to-string conversion.
120 |
121 | You can customize the output by tweaking `PodiumBackend` instance,
122 | which has four methods for simple customization:
123 |
124 | `registerSimpleFormattingCode(self, name, fun)` ~
125 | *registerSimpleFormattingCode()*
126 |
127 | This method registers a simple formatting code, e.g., `B<...>`.
128 | `name` is the name of the formatting code: the single capital letter.
129 | `fun` is a function that takes a string and returns a string.
130 |
131 | >
132 | local podium = require('podium')
133 | local inputString = "..."
134 | local backend = podium.html -- or markdown, latex, vimdoc
135 | backend:registerSimpleFormattingCode('B', function (text)
136 | return '' .. text .. ' '
137 | end)
138 | print(podium.process(backend, inputString)) -- process returns output string
139 | <
140 |
141 | `registerSimpleCommand(self, name, fun)` ~
142 | *registerSimpleCommand()*
143 |
144 | This method registers a simple command, e.g., `=head1 ...`.
145 | `name` is the name of the command defined in the POD document.
146 | Please do not override `=begin` and `=end` commands.
147 |
148 | >
149 | local podium = require('podium')
150 | local inputString = "..."
151 | local backend = podium.html -- or markdown, latex, vimdoc
152 | backend:registerSimpleCommand('head1', function (text)
153 | return '' .. text .. ' '
154 | end)
155 | print(podium.process(backend, inputString)) -- process returns output string
156 | <
157 |
158 | `registerSimpleDataParagraph(self, name, fun)` ~
159 | *registerSimpleDataParagraph()*
160 |
161 | This method registers a simple data paragraph, e.g., content between
162 | `=begin name` and `=end name` commands.
163 | `name` is the name of the data paragraph, e.g., html or markdown.
164 |
165 | >
166 | local podium = require('podium')
167 | local inputString = "..."
168 | local backend = podium.html -- or markdown, latex, vimdoc
169 | backend:registerSimpleDataParagraph('html', function (text)
170 | return '' .. text .. '
'
171 | end)
172 | print(podium.process(backend, inputString)) -- process returns output string
173 | <
174 |
175 | `registerSimple(self, name, fun)` ~
176 | *registerSimple()*
177 |
178 | This method is used to register another simple conversion, e.g., preamble, and
179 | postambles. `name` is the name of the conversion, e.g., preamble, and
180 | postambles. `fun` is a function that takes a string and returns a string.
181 | The function takes the entire input string as an argument for preamble and
182 | postambles.
183 |
184 | >
185 | local podium = require('podium')
186 | local inputString = "..."
187 | local backend = podium.html -- or markdown, latex, vimdoc
188 | backend
189 | :registerSimple('preamble', function (text)
190 | return ''
191 | end)
192 | :registerSimple('postamble', function (text)
193 | return ''
194 | end)
195 | print(podium.process(html, inputString)) -- process returns output string
196 | <
197 |
198 | JavaScript API ~
199 | *podium-js-api*
200 |
201 | Podium is written in Lua, but you can use it in JavaScript as well.
202 |
203 | >
204 | import {LuaFactory} from 'npm:wasmoon@latest';
205 | const luaFactory = new LuaFactory();
206 | const lua = await luaFactory.createEngine();
207 | const code = Deno.readTextFileSync('./lua/podium.lua').replace("#!/usr/bin/env lua", "")
208 | const pod = await lua.doString(code);
209 |
210 | // Arguments: backend name, node name, modifier function
211 | pod.PodiumBackend.registerSimpleDataParagraph("html", "red", (arg) => {
212 | return `${red}`
213 | })
214 |
215 | // Arguments: backend name, input string
216 | // Returns: output string
217 | pod.process("html", "...")
218 | <
219 |
220 | License ~
221 | *podium-license*
222 |
223 | Licensed under MIT License.
224 |
225 | Copyright (c) 2022 TANIGUCHI Masaya
226 |
227 |
228 | vim:tw=78:ts=8:noet:ft=help:norl:
229 |
--------------------------------------------------------------------------------
/spec/podium_spec.lua:
--------------------------------------------------------------------------------
1 | local pod = require("./podium")
2 |
3 | local function unindent(str)
4 | local lines = pod.splitLines(pod.PodiumElement.new(str))
5 | local indent = lines[1]:match("^%s*")
6 | for i, line in ipairs(lines) do
7 | lines[i] = line:gsub("^" .. indent, "")
8 | end
9 | return table.concat(lines)
10 | end
11 |
12 | describe("POD Parser", function()
13 | describe(":trim", function()
14 | it("trims string", function()
15 | local source = " foo "
16 | local actual = pod.PodiumElement.new(source):trim().value
17 | local expected = "foo"
18 | assert.are.same(expected, actual)
19 | end)
20 | it("trims string with newline", function()
21 | local source = " foo \n "
22 | local actual = pod.PodiumElement.new(source):trim().value
23 | local expected = "foo"
24 | assert.are.same(expected, actual)
25 | end)
26 | end)
27 | describe("findFormattingCode", function()
28 | it("finds formatting code", function()
29 | local source = "C"
30 | local b_cmd, b_arg, e_arg, e_cmd =
31 | pod.findFormattingCode(pod.PodiumElement.new(source))
32 | assert.are.same({ 1, 3, 5, 6 }, { b_cmd, b_arg, e_arg, e_cmd })
33 | end)
34 | end)
35 | describe("findDataParagraph", function()
36 | it("finds data paragraph", function()
37 | local source = unindent([[
38 | =begin html
39 |
40 | This is html paragraph
41 |
42 | =end html
43 | ]])
44 | local b_cmd, b_arg, e_arg, e_cmd =
45 | pod.findDataParagraph(pod.PodiumElement.new(source))
46 | assert.are.same({ 1, 13, 44, 54 }, { b_cmd, b_arg, e_arg, e_cmd })
47 | end)
48 | end)
49 | describe("splitList function", function()
50 | it("splits simple indent block with default indent", function()
51 | local source = unindent([[
52 | =over
53 |
54 | hoge
55 |
56 | =back
57 | ]])
58 | local actual = pod.splitList(pod.PodiumElement.new(source))
59 | local expected = {
60 | {
61 | kind = "over",
62 | value = unindent([[
63 | =over
64 |
65 | ]]),
66 | source = source,
67 | startIndex = 1,
68 | endIndex = 7,
69 | indentLevel = 4,
70 | extraProps = {
71 | listStyle = "unordered",
72 | listDepth = 1,
73 | },
74 | },
75 | {
76 | kind = "items",
77 | value = unindent([[
78 | hoge
79 |
80 | ]]),
81 | source = source,
82 | startIndex = 8,
83 | endIndex = 13,
84 | indentLevel = 4,
85 | extraProps = {
86 | listDepth = 1,
87 | },
88 | },
89 | {
90 | kind = "backspace",
91 | source = source,
92 | startIndex = 1,
93 | endIndex = 19,
94 | indentLevel = 0,
95 | extraProps = {
96 | listDepth = 1,
97 | deleteCount = 4,
98 | },
99 | value = source,
100 | },
101 | {
102 | kind = "back",
103 | value = unindent([[
104 | =back
105 | ]]),
106 | source = source,
107 | startIndex = 14,
108 | endIndex = 19,
109 | indentLevel = 0,
110 | extraProps = {
111 | listDepth = 1,
112 | listStyle = "unordered",
113 | },
114 | },
115 | }
116 | assert.are.same(expected, actual)
117 | end)
118 | it("splits simple indent block", function()
119 | local source = unindent([[
120 | =over 8
121 |
122 | hoge
123 |
124 | =back
125 | ]])
126 | local actual = pod.splitList(pod.PodiumElement.new(source))
127 | local expected = {
128 | {
129 | kind = "over",
130 | value = unindent([[
131 | =over 8
132 |
133 | ]]),
134 | source = source,
135 | startIndex = 1,
136 | endIndex = 9,
137 | indentLevel = 8,
138 | extraProps = {
139 | listDepth = 1,
140 | listStyle = "unordered",
141 | },
142 | },
143 | {
144 | kind = "items",
145 | value = unindent([[
146 | hoge
147 |
148 | ]]),
149 | source = source,
150 | startIndex = 10,
151 | endIndex = 15,
152 | indentLevel = 8,
153 | extraProps = { listDepth = 1 },
154 | },
155 | {
156 | kind = "backspace",
157 | source = source,
158 | startIndex = 1,
159 | endIndex = 21,
160 | indentLevel = 0,
161 | extraProps = { listDepth = 1, deleteCount = 8 },
162 | value = source,
163 | },
164 | {
165 | kind = "back",
166 | value = unindent([[
167 | =back
168 | ]]),
169 | source = source,
170 | startIndex = 16,
171 | endIndex = 21,
172 | indentLevel = 0,
173 | extraProps = { listDepth = 1, listStyle = "unordered" },
174 | },
175 | }
176 | assert.are.same(expected, actual)
177 | end)
178 | it("splits nested indent block", function()
179 | local source = unindent([[
180 | =over 8
181 |
182 | hoge
183 |
184 | =over 4
185 |
186 | =item fuga
187 |
188 | =back
189 |
190 | =back
191 | ]])
192 | local actual = pod.splitList(pod.PodiumElement.new(source))
193 | local expected = {
194 | {
195 | kind = "over",
196 | value = unindent([[
197 | =over 8
198 |
199 | ]]),
200 | source = source,
201 | startIndex = 1,
202 | endIndex = 9,
203 | indentLevel = 8,
204 | extraProps = { listDepth = 1, listStyle = "unordered" },
205 | },
206 | {
207 | kind = "items",
208 | value = unindent([[
209 | hoge
210 |
211 | =over 4
212 |
213 | =item fuga
214 |
215 | =back
216 |
217 | ]]),
218 | source = source,
219 | startIndex = 10,
220 | endIndex = 43,
221 | indentLevel = 8,
222 | extraProps = { listDepth = 1 },
223 | },
224 | {
225 | kind = "backspace",
226 | source = source,
227 | startIndex = 1,
228 | endIndex = 49,
229 | indentLevel = 0,
230 | extraProps = { listDepth = 1, deleteCount = 8 },
231 | value = source,
232 | },
233 | {
234 | kind = "back",
235 | value = unindent([[
236 | =back
237 | ]]),
238 | source = source,
239 | startIndex = 44,
240 | endIndex = 49,
241 | indentLevel = 0,
242 | extraProps = { listDepth = 1, listStyle = "unordered" },
243 | },
244 | }
245 | assert.are.same(expected, actual)
246 | end)
247 | end)
248 | describe("splitLines function", function()
249 | it("splits lines by \\n", function()
250 | local source = "foo\nbar\nbazz"
251 | local actual = pod.splitLines(pod.PodiumElement.new(source))
252 | local expected = { "foo\n", "bar\n", "bazz" }
253 | assert.are.same(expected, actual)
254 | end)
255 | it("splits lines by \\r", function()
256 | local source = "foo\rbar\rbazz"
257 | local actual = pod.splitLines(pod.PodiumElement.new(source))
258 | local expected = { "foo\r", "bar\r", "bazz" }
259 | assert.are.same(expected, actual)
260 | end)
261 | it("splits lines by \\r\\n", function()
262 | local source = "foo\r\nbar\r\nbazz"
263 | local actual = pod.splitLines(pod.PodiumElement.new(source))
264 | local expected = { "foo\r\n", "bar\r\n", "bazz" }
265 | assert.are.same(expected, actual)
266 | end)
267 | it("splits lines by mixed newline characters", function()
268 | local source = "foo\r\nbar\rbazz\nhoge"
269 | local actual = pod.splitLines(pod.PodiumElement.new(source))
270 | local expected = { "foo\r\n", "bar\r", "bazz\n", "hoge" }
271 | assert.are.same(expected, actual)
272 | end)
273 | end)
274 |
275 | describe("splitParagraphs function", function()
276 | it("splits headings", function()
277 | local source = unindent([[
278 | =head1 foo
279 |
280 |
281 | =head2 hoge
282 |
283 | =head3 bar
284 |
285 | bar
286 | ]])
287 | local actual = pod.splitParagraphs(pod.PodiumElement.new(source))
288 | local expected = {
289 | {
290 | kind = "head1",
291 | value = unindent([[
292 | =head1 foo
293 |
294 | ]]),
295 | source = source,
296 | startIndex = 1,
297 | endIndex = 12,
298 | indentLevel = 0,
299 | extraProps = {},
300 | },
301 | {
302 | kind = "head2",
303 | value = unindent([[
304 | =head2 hoge
305 |
306 | ]]),
307 | source = source,
308 | startIndex = 14,
309 | endIndex = 26,
310 | indentLevel = 0,
311 | extraProps = {},
312 | },
313 | {
314 | kind = "head3",
315 | value = unindent([[
316 | =head3 bar
317 |
318 | ]]),
319 | source = source,
320 | startIndex = 27,
321 | endIndex = 38,
322 | indentLevel = 0,
323 | extraProps = {},
324 | },
325 | {
326 | kind = "paragraph",
327 | value = "bar\n",
328 | source = source,
329 | startIndex = 39,
330 | endIndex = 42,
331 | indentLevel = 0,
332 | extraProps = {},
333 | },
334 | }
335 | assert.are.same(expected, actual)
336 | end)
337 | it("splits paragraphs by empty line", function()
338 | local source = "foo\n\nbar\n\nbazz"
339 | local actual = pod.splitParagraphs(pod.PodiumElement.new(source))
340 | local expected = {
341 | {
342 | kind = "paragraph",
343 | value = unindent([[
344 | foo
345 |
346 | ]]),
347 | source = source,
348 | startIndex = 1,
349 | endIndex = 5,
350 | indentLevel = 0,
351 | extraProps = {},
352 | },
353 | {
354 | kind = "paragraph",
355 | value = unindent([[
356 | bar
357 |
358 | ]]),
359 | source = source,
360 | startIndex = 6,
361 | endIndex = 10,
362 | indentLevel = 0,
363 | extraProps = {},
364 | },
365 | {
366 | kind = "paragraph",
367 | value = "bazz",
368 | source = source,
369 | startIndex = 11,
370 | endIndex = 14,
371 | indentLevel = 0,
372 | extraProps = {},
373 | },
374 | }
375 | assert.are.same(expected, actual)
376 | end)
377 | it("splits paragraphs by over-back block", function()
378 | local source = unindent([[
379 | foo
380 |
381 | =over
382 |
383 | =item bar
384 |
385 | =item bazz
386 |
387 | =back
388 |
389 | hoge]])
390 | local actual = pod.splitParagraphs(pod.PodiumElement.new(source))
391 | local expected = {
392 | {
393 | kind = "paragraph",
394 | value = unindent([[
395 | foo
396 |
397 | ]]),
398 | source = source,
399 | startIndex = 1,
400 | endIndex = 5,
401 | indentLevel = 0,
402 | extraProps = {},
403 | },
404 | {
405 | kind = "list",
406 | value = unindent([[
407 | =over
408 |
409 | =item bar
410 |
411 | =item bazz
412 |
413 | =back
414 |
415 | ]]),
416 | source = source,
417 | startIndex = 6,
418 | endIndex = 42,
419 | indentLevel = 0,
420 | extraProps = {},
421 | },
422 | {
423 | kind = "paragraph",
424 | value = "hoge",
425 | source = source,
426 | startIndex = 43,
427 | endIndex = 46,
428 | indentLevel = 0,
429 | extraProps = {},
430 | },
431 | }
432 | assert.are.same(expected, actual)
433 | end)
434 | it("splits paragraphs by nested over-back block", function()
435 | local source = unindent([[
436 | foo
437 |
438 | =over
439 |
440 | =item bar
441 |
442 | =over
443 |
444 | =item bazz
445 |
446 | =back
447 |
448 | =item hoge
449 |
450 | =back
451 |
452 | fuga]])
453 | local actual = pod.splitParagraphs(pod.PodiumElement.new(source))
454 | local expected = {
455 | {
456 | kind = "paragraph",
457 | value = unindent([[
458 | foo
459 |
460 | ]]),
461 | source = source,
462 | startIndex = 1,
463 | endIndex = 5,
464 | indentLevel = 0,
465 | extraProps = {},
466 | },
467 | {
468 | kind = "list",
469 | value = unindent([[
470 | =over
471 |
472 | =item bar
473 |
474 | =over
475 |
476 | =item bazz
477 |
478 | =back
479 |
480 | =item hoge
481 |
482 | =back
483 |
484 | ]]),
485 | source = source,
486 | startIndex = 6,
487 | endIndex = 68,
488 | indentLevel = 0,
489 | extraProps = {},
490 | },
491 | {
492 | kind = "paragraph",
493 | value = "fuga",
494 | source = source,
495 | startIndex = 69,
496 | endIndex = 72,
497 | indentLevel = 0,
498 | extraProps = {},
499 | },
500 | }
501 | assert.are.same(expected, actual)
502 | end)
503 | it("splits paragraphs by begin-end block", function()
504 | local source = unindent([[
505 | foo
506 |
507 | =begin html
508 |
509 | bar
510 |
511 | =end html
512 |
513 | bar]])
514 | local actual = pod.splitParagraphs(pod.PodiumElement.new(source))
515 | local expected = {
516 | {
517 | kind = "paragraph",
518 | value = unindent([[
519 | foo
520 |
521 | ]]),
522 | source = source,
523 | startIndex = 1,
524 | endIndex = 5,
525 | indentLevel = 0,
526 | extraProps = {},
527 | },
528 | {
529 | kind = "data",
530 | value = unindent([[
531 | =begin html
532 |
533 | bar
534 |
535 | =end html
536 |
537 | ]]),
538 | source = source,
539 | startIndex = 6,
540 | endIndex = 41,
541 | indentLevel = 0,
542 | extraProps = { dataKind = "html" },
543 | },
544 | {
545 | kind = "paragraph",
546 | value = "bar",
547 | source = source,
548 | startIndex = 42,
549 | endIndex = 44,
550 | indentLevel = 0,
551 | extraProps = {},
552 | },
553 | }
554 | assert.are.same(expected, actual)
555 | end)
556 | it("does not split lines with no empty line", function()
557 | local source = "foo\nbar\nbazz"
558 | local actual = pod.splitParagraphs(pod.PodiumElement.new(source))
559 | local expected = {
560 | {
561 | kind = "paragraph",
562 | value = unindent([[
563 | foo
564 | bar
565 | bazz]]),
566 | source = source,
567 | startIndex = 1,
568 | endIndex = 12,
569 | indentLevel = 0,
570 | extraProps = {},
571 | },
572 | }
573 | assert.are.same(expected, actual)
574 | end)
575 | it("does not split lines with no empty line", function()
576 | local source = unindent([[
577 | foo
578 |
579 | =over
580 |
581 | =item bar
582 |
583 | =item bazz
584 |
585 | =back
586 | hoge]])
587 | local actual = pod.splitParagraphs(pod.PodiumElement.new(source))
588 | local expected = {
589 | {
590 | kind = "paragraph",
591 | value = unindent([[
592 | foo
593 |
594 | ]]),
595 | source = source,
596 | startIndex = 1,
597 | endIndex = 5,
598 | indentLevel = 0,
599 | extraProps = {},
600 | },
601 | {
602 | kind = "list",
603 | value = unindent([[
604 | =over
605 |
606 | =item bar
607 |
608 | =item bazz
609 |
610 | =back
611 | hoge]]),
612 | source = source,
613 | startIndex = 6,
614 | endIndex = 45,
615 | indentLevel = 0,
616 | extraProps = {},
617 | },
618 | }
619 | assert.are.same(expected, actual)
620 | end)
621 | end)
622 | describe("splitItems function", function()
623 | it("split no items", function()
624 | local source = unindent([[
625 | lorem ipsum
626 | dolor sit amet]])
627 | local actual = pod.splitItems(pod.PodiumElement.new(source))
628 | local expected = {
629 | {
630 | kind = "paragraph",
631 | value = unindent([[
632 | lorem ipsum
633 | dolor sit amet]]),
634 | source = source,
635 | startIndex = 1,
636 | endIndex = 26,
637 | indentLevel = 0,
638 | extraProps = {},
639 | },
640 | }
641 | assert.are.same(expected, actual)
642 | end)
643 | it("split no items with list", function()
644 | local source = unindent([[
645 | lorem ipsum
646 |
647 | =over
648 | =item hoge
649 | =item fuga
650 | =back
651 |
652 | dolor sit amet]])
653 | local actual = pod.splitItems(pod.PodiumElement.new(source))
654 | local expected = {
655 | {
656 | kind = "paragraph",
657 | value = unindent([[
658 | lorem ipsum
659 |
660 | ]]),
661 | source = source,
662 | startIndex = 1,
663 | endIndex = 13,
664 | indentLevel = 0,
665 | extraProps = {},
666 | },
667 | {
668 | kind = "list",
669 | value = unindent([[
670 | =over
671 | =item hoge
672 | =item fuga
673 | =back
674 |
675 | ]]),
676 | source = source,
677 | startIndex = 14,
678 | endIndex = 48,
679 | indentLevel = 0,
680 | extraProps = {},
681 | },
682 | {
683 | kind = "paragraph",
684 | value = "dolor sit amet",
685 | source = source,
686 | startIndex = 49,
687 | endIndex = 62,
688 | indentLevel = 0,
689 | extraProps = {},
690 | },
691 | }
692 | assert.are.same(expected, actual)
693 | end)
694 | it("split items", function()
695 | local source = unindent([[
696 | =item foo
697 |
698 | =item bar
699 |
700 | =item bazz]])
701 | local actual = pod.splitItems(pod.PodiumElement.new(source))
702 | local expected = {
703 | {
704 | kind = "item",
705 | value = unindent([[
706 | =item foo
707 |
708 | ]]),
709 | source = source,
710 | startIndex = 1,
711 | endIndex = 11,
712 | indentLevel = 0,
713 | extraProps = {},
714 | },
715 | {
716 | kind = "item",
717 | value = unindent([[
718 | =item bar
719 |
720 | ]]),
721 | source = source,
722 | startIndex = 12,
723 | endIndex = 22,
724 | indentLevel = 0,
725 | extraProps = {},
726 | },
727 | {
728 | kind = "item",
729 | value = unindent([[
730 | =item bazz]]),
731 | source = source,
732 | startIndex = 23,
733 | endIndex = 32,
734 | indentLevel = 0,
735 | extraProps = {},
736 | },
737 | }
738 | assert.are.same(expected, actual)
739 | end)
740 | it("split items with nested list", function()
741 | local source = unindent([[
742 | =item foo
743 |
744 | =over
745 | =item bar
746 |
747 | =item bazz
748 | =back
749 |
750 | =item hoge]])
751 | local actual = pod.splitItems(pod.PodiumElement.new(source))
752 | local expected = {
753 | {
754 | kind = "item",
755 | value = unindent([[
756 | =item foo
757 |
758 | =over
759 | =item bar
760 |
761 | =item bazz
762 | =back
763 |
764 | ]]),
765 | source = source,
766 | startIndex = 1,
767 | endIndex = 46,
768 | indentLevel = 0,
769 | extraProps = {},
770 | },
771 | {
772 | kind = "item",
773 | value = "=item hoge",
774 | source = source,
775 | startIndex = 47,
776 | endIndex = 56,
777 | indentLevel = 0,
778 | extraProps = {},
779 | },
780 | }
781 | assert.are.same(expected, actual)
782 | end)
783 | end)
784 |
785 | describe("splitItem function", function()
786 | it("split parts", function()
787 | local source = "=item foo bar"
788 | local actual = pod.splitItem(pod.PodiumElement.new(source))
789 | local expected = {
790 | {
791 | kind = "itempart",
792 | value = "=item foo bar",
793 | source = source,
794 | startIndex = 1,
795 | endIndex = 13,
796 | indentLevel = 0,
797 | extraProps = {},
798 | },
799 | }
800 | assert.are.same(expected, actual)
801 | end)
802 | it("splits parts with begin - end", function()
803 | local source = unindent([[
804 | =item foo
805 | bar
806 | =over
807 | =item
808 | =back
809 |
810 | bazz]])
811 | local actual = pod.splitItem(pod.PodiumElement.new(source))
812 | local expected = {
813 | {
814 | kind = "itempart",
815 | value = unindent([[
816 | =item foo
817 | bar]]),
818 | source = source,
819 | startIndex = 1,
820 | endIndex = 13,
821 | indentLevel = 0,
822 | extraProps = {},
823 | },
824 | {
825 | kind = "list",
826 | value = unindent([[
827 | =over
828 | =item
829 | =back]]),
830 | source = source,
831 | startIndex = 15,
832 | endIndex = 31,
833 | indentLevel = 0,
834 | extraProps = {},
835 | },
836 | {
837 | kind = "itempart",
838 | value = "bazz",
839 | source = source,
840 | startIndex = 34,
841 | endIndex = 37,
842 | indentLevel = 0,
843 | extraProps = {},
844 | },
845 | }
846 | assert.are.same(expected, actual)
847 | end)
848 | end)
849 | describe("splitTokens function", function()
850 | it("split tokens without cmd", function()
851 | local source = "foo bar"
852 | local actual = pod.splitTokens(pod.PodiumElement.new(source))
853 | local expected = {
854 | {
855 | kind = "text",
856 | value = "foo bar",
857 | source = source,
858 | startIndex = 1,
859 | endIndex = 7,
860 | indentLevel = 0,
861 | extraProps = {},
862 | },
863 | }
864 | assert.are.same(expected, actual)
865 | end)
866 | it("split tokens with cmd", function()
867 | local source = "foo bar C huga"
868 | local actual = pod.splitTokens(pod.PodiumElement.new(source))
869 | local expected = {
870 | {
871 | kind = "text",
872 | value = "foo bar ",
873 | source = source,
874 | startIndex = 1,
875 | endIndex = 8,
876 | indentLevel = 0,
877 | extraProps = {},
878 | },
879 | {
880 | kind = "C",
881 | value = "C",
882 | source = source,
883 | startIndex = 9,
884 | endIndex = 15,
885 | indentLevel = 0,
886 | extraProps = {},
887 | },
888 | {
889 | kind = "text",
890 | value = " huga",
891 | source = source,
892 | startIndex = 16,
893 | endIndex = 20,
894 | indentLevel = 0,
895 | extraProps = {},
896 | },
897 | }
898 | assert.are.same(expected, actual)
899 | end)
900 | end)
901 | end)
902 |
--------------------------------------------------------------------------------
/lua/podium.lua:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lua
2 |
3 | ---@diagnostic disable: unused-local
4 | ---@diagnostic disable: unused-function
5 |
6 | --[[
7 | MIT License
8 |
9 | Copyright (c) 2023 TANIGUCHI Masaya
10 |
11 | Permission is hereby granted, free of charge, to any person obtaining a copy
12 | of this software and associated documentation files (the "Software"), to deal
13 | in the Software without restriction, including without limitation the rights
14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 | copies of the Software, and to permit persons to whom the Software is
16 | furnished to do so, subject to the following conditions:
17 |
18 | The above copyright notice and this permission notice shall be included in all
19 | copies or substantial portions of the Software.
20 |
21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 | SOFTWARE.
28 | ]]
29 |
30 | local M = {}
31 | local _ -- dummy
32 |
33 | ---@alias PodiumElementKindInlineCmd
34 | ---| 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M'
35 | ---| 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z'
36 | ---@alias PodiumElementKindBlockCmd -- All block commands defined in POD spec
37 | ---| 'pod'
38 | ---| 'cut'
39 | ---| 'encoding'
40 | ---| 'over'
41 | ---| 'item'
42 | ---| 'back'
43 | ---| 'verbatim'
44 | ---| 'for'
45 | ---| 'head1'
46 | ---| 'head2'
47 | ---| 'head3'
48 | ---| 'head4'
49 | ---@alias PodiumElementKindInternalConstituent -- All internal constituents
50 | ---| 'backspace'
51 | ---| 'list'
52 | ---| 'items'
53 | ---| 'itempart'
54 | ---| 'paragraph'
55 | ---| 'text'
56 | ---| 'preamble'
57 | ---| 'postamble'
58 | ---@alias PodiumElementKind
59 | ---| PodiumElementKindBlockCmd
60 | ---| PodiumElementKindInlineCmd
61 | ---| PodiumElementKindInternalConstituent
62 | ---| string if you want to add custom block comando, use this type
63 | ---@class PodiumElement
64 | ---@field kind PodiumElementKind The kind of the element
65 | ---@field value string The content text of the element
66 | ---@field source string The source text of the element
67 | ---@field startIndex integer The index of the first character of the element in the source text.
68 | ---@field endIndex integer The index of the last character of the element in the source text.
69 | ---@field indentLevel integer The first character of the line following a line break is indented at this indent size.
70 | ---@field extraProps table The extra properties of the element
71 | ---@field clone fun(self: PodiumElement, props?: table): PodiumElement
72 | local PodiumElement = {}
73 |
74 | ---@param self PodiumElement
75 | ---@param props? table
76 | ---@return PodiumElement
77 | function PodiumElement.clone(self, props)
78 | props = props or {}
79 | return setmetatable({
80 | source = props.source or self.source,
81 | startIndex = props.startIndex or self.startIndex,
82 | endIndex = props.endIndex or self.endIndex,
83 | indentLevel = props.indentLevel or self.indentLevel,
84 | kind = props.kind or self.kind,
85 | value = props.value or self.value,
86 | extraProps = props.extraProps or self.extraProps,
87 | }, { __index = PodiumElement })
88 | end
89 |
90 | ---@param source string The source text of the element
91 | ---@param startIndex? integer (default: 1) The index of the first character of the element in the source text.
92 | ---@param endIndex? integer (default: #source) The index of the last character of the element in the source text.
93 | ---@param indentLevel? integer (default: 0) The first character of the line following a line break is indented at this indent size.
94 | ---@param value? string (default: source) The content text of the element
95 | ---@param kind? PodiumElementKind (default: "text") The kind of the element
96 | ---@param extraProps? table (default: {}) The extra properties of the element
97 | ---@return PodiumElement
98 | function PodiumElement.new(
99 | source,
100 | startIndex,
101 | endIndex,
102 | indentLevel,
103 | kind,
104 | value,
105 | extraProps
106 | )
107 | startIndex = startIndex or 1
108 | endIndex = endIndex or #source
109 | indentLevel = indentLevel or 0
110 | kind = kind or "text"
111 | value = value or source:sub(startIndex, endIndex)
112 | extraProps = extraProps or {}
113 | return setmetatable({
114 | source = source,
115 | startIndex = startIndex,
116 | endIndex = endIndex,
117 | indentLevel = indentLevel,
118 | kind = kind,
119 | value = value,
120 | extraProps = extraProps,
121 | }, { __index = PodiumElement })
122 | end
123 |
124 | ---@param self PodiumElement
125 | ---@param pattern string
126 | ---@param startIndex? integer
127 | ---@param endIndex? integer
128 | ---@param plain? boolean
129 | ---@return integer, integer, ...any
130 | function PodiumElement.find(self, pattern, startIndex, endIndex, plain)
131 | startIndex = startIndex or self.startIndex
132 | endIndex = endIndex or self.endIndex
133 | plain = plain or false
134 | return self.source:sub(1, endIndex):find(pattern, startIndex, plain)
135 | end
136 |
137 | ---@param self PodiumElement
138 | ---@param startIndex? integer
139 | ---@param endIndex? integer
140 | function PodiumElement.sub(self, startIndex, endIndex)
141 | startIndex = startIndex or self.startIndex
142 | endIndex = endIndex or self.endIndex
143 | return self:clone({
144 | startIndex = startIndex,
145 | endIndex = endIndex,
146 | value = self.source:sub(startIndex, endIndex),
147 | })
148 | end
149 |
150 | ---@param self PodiumElement
151 | ---@return PodiumElement
152 | function PodiumElement.trim(self)
153 | local startIndex, _, space = self:find("%S.*%S(%s*)")
154 | startIndex, space = startIndex or self.startIndex, space or ""
155 | return self:sub(startIndex, self.endIndex - #space)
156 | end
157 |
158 | ---@param self PodiumElement
159 | ---@return PodiumElement
160 | function PodiumElement.sanitize(self)
161 | return self:clone({
162 | value = self.value:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"),
163 | })
164 | end
165 |
166 | ---@generic T
167 | ---@param t T[]
168 | ---@param i? integer
169 | ---@param j? integer
170 | ---@return T[]
171 | local function slice(t, i, j)
172 | i = i and i > 0 and i or 1
173 | j = j and j <= #t and j or #t
174 | local r = {}
175 | for k = i, j do
176 | table.insert(r, t[k])
177 | end
178 | return r
179 | end
180 |
181 | ---@param str string
182 | ---@return string
183 | local function unindent(str)
184 | local lines = pod.splitLines(pod.PodiumElement.new(str))
185 | local indent = lines[1]:match("^%s*")
186 | for i, line in ipairs(lines) do
187 | lines[i] = line:gsub("^" .. indent, "")
188 | end
189 | return table.concat(lines)
190 | end
191 |
192 | ---@generic T
193 | ---@param t T[]
194 | ---@param ... T[]
195 | ---@return T[]
196 | local function append(t, ...)
197 | local r = {}
198 | for _, v in ipairs(t) do
199 | table.insert(r, v)
200 | end
201 | for _, s in ipairs({ ... }) do
202 | for _, v in ipairs(s) do
203 | table.insert(r, v)
204 | end
205 | end
206 | return r
207 | end
208 |
209 | ---@param source string
210 | ---@return string "\r"|"\n"|"\r\n"
211 | local function guessNewline(source)
212 | local i = 1
213 | while i <= #source do
214 | local c = source:sub(i, i)
215 | if c == "\n" then
216 | return "\n"
217 | elseif c == "\r" then
218 | if source:sub(i + 1, i + 1) == "\n" then
219 | return "\r\n"
220 | else
221 | return "\r"
222 | end
223 | end
224 | i = i + 1
225 | end
226 | return "\n"
227 | end
228 |
229 | ---@param element PodiumElement
230 | ---@return string[]
231 | local function splitLines(element)
232 | ---@type string[]
233 | local lines = {}
234 | local i = element.startIndex
235 | while i <= element.endIndex do
236 | local j = element:find("[\r\n]", i)
237 | if j == nil then
238 | table.insert(lines, element:sub(i).value)
239 | i = element.endIndex + 1
240 | else
241 | if element:sub(j, j).value == "\r" then
242 | if element:sub(j + 1, j + 1).value == "\n" then
243 | j = j + 1
244 | end
245 | end
246 | table.insert(lines, element:sub(i, j).value)
247 | i = j + 1
248 | end
249 | end
250 | return lines
251 | end
252 |
253 | ---@param source string
254 | ---@return table
255 | local function parseFrontMatter(source)
256 | ---@type table
257 | local frontmatter = {}
258 | local lines = splitLines(PodiumElement.new(source, 1, #source, 0))
259 | local inside = false
260 | for _, line in ipairs(lines) do
261 | if inside then
262 | if line:match("^%s*---%s*$") then
263 | break
264 | else
265 | local key, value = line:match("^%s*(%w-)%s*:%s*(.-)%s*$")
266 | if key then
267 | frontmatter[key] = value
268 | end
269 | end
270 | else
271 | if line:match("^%s*---%s*$") then
272 | inside = true
273 | end
274 | end
275 | end
276 | return frontmatter
277 | end
278 |
279 | ---@param source string
280 | ---@param startIndex integer
281 | ---@return integer,integer
282 | local function indexToRowCol(source, startIndex)
283 | local row = 1
284 | local col = 1
285 | local i = 1
286 | while i < startIndex do
287 | local c = source:sub(i, i)
288 | if c == "\n" then
289 | row = row + 1
290 | col = 1
291 | elseif c == "\r" then
292 | row = row + 1
293 | col = 1
294 | if source:sub(i + 1, i + 1) == "\n" then
295 | i = i + 1
296 | end
297 | else
298 | col = col + 1
299 | end
300 | i = i + 1
301 | end
302 | return row, col
303 | end
304 |
305 | ---@param element PodiumElement
306 | ---@return integer,integer,integer,integer
307 | local function findFormattingCode(element)
308 | for b_cmd = element.startIndex, element.endIndex do
309 | if element.source:sub(b_cmd, b_cmd):match("[A-Z]") then
310 | if element.source:sub(b_cmd + 1, b_cmd + 1) == "<" then
311 | local count = 1
312 | local space = ""
313 | local i = b_cmd + 2
314 | local b_arg, e_arg = nil, nil
315 | while i <= element.endIndex do
316 | if element.source:sub(i, i) == "<" then
317 | count = count + 1
318 | i = i + 1
319 | elseif element.source:sub(i, i):match("%s") then
320 | b_arg = i + 1
321 | space = "%s"
322 | break
323 | else
324 | b_arg = b_cmd + 2
325 | count = 1
326 | break
327 | end
328 | end
329 | if i > element.endIndex then
330 | local row, col = indexToRowCol(element.source, b_cmd)
331 | error(
332 | "ERROR:"
333 | .. row
334 | .. ":"
335 | .. col
336 | .. ": "
337 | .. "Missing closing brackets '<"
338 | .. string.rep(">", count)
339 | .. "'"
340 | )
341 | end
342 | local angles = space .. string.rep(">", count)
343 | while i <= element.endIndex do
344 | if element.source:sub(i, i + #angles - 1):match(angles) then
345 | e_arg = i - 1
346 | break
347 | end
348 | if element.source:sub(i, i) == "<" then
349 | if element.source:sub(i - 1, i - 1):match("[A-Z]") then
350 | _, _, _, i =
351 | findFormattingCode(element:sub(i - 1, element.endIndex))
352 | end
353 | end
354 | i = i + 1
355 | end
356 | if i > element.endIndex then
357 | local row, col = indexToRowCol(element.source, b_cmd)
358 | error(
359 | "Missing closing brackets '"
360 | .. string.rep(">", count)
361 | .. "':"
362 | .. row
363 | .. ":"
364 | .. col
365 | .. ": "
366 | .. element.source:sub(b_cmd, b_cmd + count)
367 | )
368 | end
369 | return b_cmd, b_arg, e_arg, i + #angles - 1
370 | end
371 | end
372 | end
373 | error("Failed to find inline command")
374 | end
375 |
376 | ---@type PodiumBackendRule
377 | local function splitParagraphs(element)
378 | local state_list = 0
379 | local state_para = 0
380 | local state_verb = 0
381 | local state_block = 0
382 | local block_name = ""
383 | local state_cmd = 0
384 | local cmd_name = ""
385 | ---@type PodiumElement[]
386 | local paragraphs = {}
387 | ---@type string[]
388 | local lines = {}
389 | local startIndex = element.startIndex
390 | for _, line in ipairs(splitLines(element)) do
391 | if state_list > 0 then
392 | table.insert(lines, line)
393 | if line:match("^=over") then
394 | state_list = state_list + 1
395 | elseif line:match("^=back") then
396 | state_list = state_list - 1
397 | elseif state_list == 1 and line:match("^%s+$") then
398 | local endIndex = startIndex + #table.concat(lines) - 1
399 | table.insert(
400 | paragraphs,
401 | element:sub(startIndex, endIndex):clone({ kind = "list" })
402 | )
403 | startIndex = endIndex + 1
404 | state_list = 0
405 | lines = {}
406 | end
407 | elseif state_para > 0 then
408 | table.insert(lines, line)
409 | if state_para == 1 and line:match("^%s+$") then
410 | local endIndex = startIndex + #table.concat(lines) - 1
411 | table.insert(
412 | paragraphs,
413 | element:sub(startIndex, endIndex):clone({
414 | kind = "paragraph",
415 | })
416 | )
417 | startIndex = endIndex + 1
418 | state_para = 0
419 | lines = {}
420 | end
421 | elseif state_verb > 0 then
422 | if state_verb == 1 and line:match("^%S") then
423 | local endIndex = startIndex + #table.concat(lines) - 1
424 | table.insert(
425 | paragraphs,
426 | element:sub(startIndex, endIndex):clone({
427 | kind = "verbatim",
428 | })
429 | )
430 | startIndex = endIndex + 1
431 | lines = { line }
432 | state_verb = 0
433 | if line:match("^=over") then
434 | state_list = 2
435 | elseif line:match("^=begin") then
436 | state_block = 2
437 | block_name = line:match("^=begin%s+(%S+)")
438 | elseif line:match("^=") then
439 | state_cmd = 1
440 | cmd_name = line:match("^=(%S+)")
441 | else
442 | state_para = 1
443 | end
444 | else
445 | table.insert(lines, line)
446 | if line:match("^%s+$") then
447 | state_verb = 1
448 | end
449 | end
450 | elseif state_block > 0 then
451 | table.insert(lines, line)
452 | if line:match("^=end%s+" .. block_name) then
453 | state_block = 1
454 | end
455 | if state_block == 1 and line:match("^%s+$") then
456 | local endIndex = startIndex + #table.concat(lines) - 1
457 | table.insert(
458 | paragraphs,
459 | element:sub(startIndex, endIndex):clone({
460 | kind = "data",
461 | extraProps = { dataKind = block_name },
462 | })
463 | )
464 | startIndex = endIndex + 1
465 | lines = {}
466 | state_block = 0
467 | end
468 | elseif state_cmd > 0 then
469 | table.insert(lines, line)
470 | if state_cmd == 1 and line:match("^%s+$") then
471 | local endIndex = startIndex + #table.concat(lines) - 1
472 | table.insert(
473 | paragraphs,
474 | element:sub(startIndex, endIndex):clone({
475 | kind = cmd_name,
476 | })
477 | )
478 | startIndex = endIndex + 1
479 | lines = {}
480 | state_cmd = 0
481 | end
482 | else
483 | if line:match("^%s+$") then
484 | local endIndex = startIndex + #line - 1
485 | startIndex = endIndex + 1
486 | elseif line:match("^=over") then
487 | table.insert(lines, line)
488 | state_list = 2
489 | elseif line:match("^=begin") then
490 | table.insert(lines, line)
491 | state_block = 2
492 | block_name = line:match("^=begin%s+(%S+)")
493 | elseif line:match("^[ \t]") then
494 | table.insert(lines, line)
495 | state_verb = 2
496 | elseif line:match("^=") then
497 | table.insert(lines, line)
498 | state_cmd = 1
499 | cmd_name = line:match("^=(%S+)")
500 | else
501 | table.insert(lines, line)
502 | state_para = 1
503 | end
504 | end
505 | end
506 | if #lines > 0 then
507 | if state_list > 0 then
508 | local endIndex = startIndex + #table.concat(lines) - 1
509 | table.insert(
510 | paragraphs,
511 | element:sub(startIndex, endIndex):clone({
512 | kind = "list",
513 | })
514 | )
515 | startIndex = endIndex + 1
516 | elseif state_para > 0 then
517 | local endIndex = startIndex + #table.concat(lines) - 1
518 | table.insert(
519 | paragraphs,
520 | element:sub(startIndex, endIndex):clone({
521 | kind = "paragraph",
522 | })
523 | )
524 | startIndex = endIndex + 1
525 | elseif state_verb > 0 then
526 | local endIndex = startIndex + #table.concat(lines) - 1
527 | table.insert(
528 | paragraphs,
529 | element:sub(startIndex, endIndex):clone({
530 | kind = "verbatim",
531 | })
532 | )
533 | startIndex = endIndex + 1
534 | elseif state_block > 0 then
535 | local endIndex = startIndex + #table.concat(lines) - 1
536 | table.insert(
537 | paragraphs,
538 | element:sub(startIndex, endIndex):clone({
539 | kind = "data",
540 | extraProps = { dataKind = block_name },
541 | })
542 | )
543 | startIndex = endIndex + 1
544 | elseif state_cmd > 0 then
545 | local endIndex = startIndex + #table.concat(lines) - 1
546 | table.insert(
547 | paragraphs,
548 | element:sub(startIndex, endIndex):clone({
549 | kind = cmd_name,
550 | })
551 | )
552 | startIndex = endIndex + 1
553 | end
554 | end
555 | return paragraphs
556 | end
557 |
558 | ---@type PodiumBackendRule
559 | local function splitItem(element)
560 | local itemState = 0
561 | ---@type string[]
562 | local lines = {}
563 | ---@type PodiumElement[]
564 | local parts = {}
565 | local startIndex = element.startIndex
566 | for _, line in ipairs(splitLines(element)) do
567 | if itemState == 0 then
568 | if line:match("^=over") then
569 | local endIndex = startIndex + #table.concat(lines) - 1
570 | table.insert(
571 | parts,
572 | element:sub(startIndex, endIndex):trim():clone({ kind = "itempart" })
573 | )
574 | startIndex = endIndex + 1
575 | itemState = itemState + 2
576 | lines = { line }
577 | else
578 | table.insert(lines, line)
579 | end
580 | else
581 | table.insert(lines, line)
582 | if line:match("^=over") then
583 | itemState = itemState + 1
584 | elseif line:match("^=back") then
585 | itemState = itemState - 1
586 | elseif itemState == 1 and line:match("^%s+$") then
587 | local endIndex = startIndex + #table.concat(lines) - 1
588 | table.insert(
589 | parts,
590 | element:sub(startIndex, endIndex):trim():clone({ kind = "list" })
591 | )
592 | startIndex = endIndex + 1
593 | lines = {}
594 | itemState = 0
595 | end
596 | end
597 | end
598 | if #lines > 0 then
599 | if itemState > 0 then
600 | local endIndex = startIndex + #table.concat(lines) - 1
601 | table.insert(
602 | parts,
603 | element:sub(startIndex, endIndex):trim():clone({ kind = "list" })
604 | )
605 | startIndex = endIndex + 1
606 | else
607 | local endIndex = startIndex + #table.concat(lines) - 1
608 | table.insert(
609 | parts,
610 | element:sub(startIndex, endIndex):trim():clone({ kind = "itempart" })
611 | )
612 | startIndex = endIndex + 1
613 | end
614 | end
615 | return parts
616 | end
617 |
618 | ---@type PodiumBackendRule
619 | local function splitItems(element)
620 | ---@type PodiumElement[]
621 | local items = {}
622 | ---@type "nonitems"|"items"
623 | local itemsState = "nonitems"
624 | local allLines = splitLines(element)
625 | ---@type string[]
626 | local lines = {}
627 | local depth = 0
628 | local index = 1
629 | local startIndex = element.startIndex
630 | while index <= #allLines do
631 | local line = allLines[index]
632 | if itemsState == "nonitems" then
633 | if line:match("^=item") then
634 | if depth == 0 then
635 | if #lines > 0 then
636 | local row, col = indexToRowCol(element.source, element.startIndex)
637 | error(
638 | "ERROR:"
639 | .. row
640 | .. ":"
641 | .. col
642 | .. ": non-item lines should not precede an item"
643 | )
644 | end
645 | lines = { line }
646 | itemsState = "items"
647 | index = index + 1
648 | else
649 | index = index + 1
650 | end
651 | elseif line:match("^=over") then
652 | depth = depth + 1
653 | index = index + 1
654 | elseif line:match("^=back") then
655 | depth = depth - 1
656 | index = index + 1
657 | else
658 | index = index + 1
659 | end
660 | elseif itemsState == "items" then
661 | if line:match("^=item") then
662 | if depth == 0 then
663 | local endIndex = startIndex + #table.concat(lines) - 1
664 | table.insert(
665 | items,
666 | element:sub(startIndex, endIndex):clone({
667 | kind = "item",
668 | })
669 | )
670 | startIndex = endIndex + 1
671 | lines = { line }
672 | index = index + 1
673 | else
674 | table.insert(lines, line)
675 | index = index + 1
676 | end
677 | elseif line:match("^=over") then
678 | depth = depth + 1
679 | table.insert(lines, line)
680 | index = index + 1
681 | elseif line:match("^=back") then
682 | depth = depth - 1
683 | table.insert(lines, line)
684 | index = index + 1
685 | else
686 | table.insert(lines, line)
687 | index = index + 1
688 | end
689 | end
690 | end
691 | if itemsState == "items" then
692 | local endIndex = startIndex + #table.concat(lines) - 1
693 | table.insert(
694 | items,
695 | element:sub(startIndex, endIndex):clone({
696 | kind = "item",
697 | })
698 | )
699 | startIndex = endIndex + 1
700 | else
701 | return splitParagraphs(element)
702 | end
703 | return items
704 | end
705 |
706 | ---@param element PodiumElement
707 | ---@param sanitize? boolean (default: false)
708 | ---@return PodiumElement[]
709 | local function splitTokens(element, sanitize)
710 | sanitize = sanitize or false
711 | ---@type PodiumElement[]
712 | local tokens = {}
713 | local i = element.startIndex
714 | while i <= element.endIndex do
715 | local ok, b_cmd, _, _, e_cmd = pcall(findFormattingCode, element:sub(i))
716 | if ok then
717 | table.insert(
718 | tokens,
719 | element:sub(i, b_cmd - 1):clone({
720 | kind = "text",
721 | })
722 | )
723 | table.insert(
724 | tokens,
725 | element:sub(b_cmd, e_cmd):clone({
726 | kind = element.source:sub(b_cmd, b_cmd),
727 | })
728 | )
729 | i = e_cmd + 1
730 | else
731 | table.insert(
732 | tokens,
733 | element:sub(i):clone({
734 | kind = "text",
735 | })
736 | )
737 | i = element.endIndex + 1
738 | end
739 | end
740 | if sanitize then
741 | for j = 1, #tokens do
742 | if tokens[j].kind == "text" then
743 | tokens[j] = tokens[j]:sanitize()
744 | end
745 | end
746 | end
747 | return tokens
748 | end
749 |
750 | ---@type PodiumBackendRule
751 | local function splitList(element)
752 | ---@type 'over' | 'items' | 'back'
753 | local listState = "over"
754 | local lines = splitLines(element)
755 | local list_type = "unordered"
756 | ---@type string[]
757 | local over_lines = {}
758 | ---@type string[]
759 | local items_lines = {}
760 | local items_depth = 0
761 | ---@type string[]
762 | local back_lines = {}
763 | local index = 1
764 | while index <= #lines do
765 | local line = lines[index]
766 | if listState == "over" then
767 | table.insert(over_lines, line)
768 | if line:match("^%s*$") then
769 | listState = "items"
770 | end
771 | index = index + 1
772 | elseif listState == "items" then
773 | if line:match("^=over") then
774 | items_depth = items_depth + 1
775 | table.insert(items_lines, line)
776 | index = index + 1
777 | elseif line:match("^=back") then
778 | items_depth = items_depth - 1
779 | if items_depth >= 0 then
780 | table.insert(items_lines, line)
781 | index = index + 1
782 | else
783 | listState = "back"
784 | end
785 | elseif line:match("^=item") then
786 | if items_depth == 0 then
787 | if line:match("^=item%s*%d+") then
788 | list_type = "ordered"
789 | end
790 | end
791 | table.insert(items_lines, line)
792 | index = index + 1
793 | else
794 | table.insert(items_lines, line)
795 | index = index + 1
796 | end
797 | else
798 | table.insert(back_lines, line)
799 | index = index + 1
800 | end
801 | end
802 | local over_endIndex = element.startIndex
803 | for _, line in ipairs(over_lines) do
804 | over_endIndex = over_endIndex + #line
805 | end
806 | over_endIndex = over_endIndex - 1
807 | local items_startIndex = over_endIndex + 1
808 | local items_endIndex = items_startIndex
809 | for _, line in ipairs(items_lines) do
810 | items_endIndex = items_endIndex + #line
811 | end
812 | items_endIndex = items_endIndex - 1
813 | local back_startIndex = items_endIndex + 1
814 | local back_endIndex = back_startIndex
815 | for _, line in ipairs(back_lines) do
816 | back_endIndex = back_endIndex + #line
817 | end
818 | back_endIndex = back_endIndex - 1
819 | local indentLevel = tonumber(table.concat(over_lines):match("(%d+)") or "4")
820 | return {
821 | element:clone({
822 | endIndex = over_endIndex,
823 | kind = "over",
824 | value = table.concat(over_lines),
825 | indentLevel = (element.indentLevel + indentLevel),
826 | extraProps = {
827 | listStyle = list_type,
828 | listDepth = (element.extraProps.listDepth or 0) + 1,
829 | },
830 | }),
831 | element:clone({
832 | startIndex = items_startIndex,
833 | endIndex = items_endIndex,
834 | kind = "items",
835 | value = table.concat(items_lines),
836 | indentLevel = (element.indentLevel + indentLevel),
837 | extraProps = {
838 | listDepth = (element.extraProps.listDepth or 0) + 1,
839 | },
840 | }),
841 | element:clone({
842 | kind = "backspace",
843 | extraProps = {
844 | deleteCount = indentLevel,
845 | listDepth = (element.extraProps.listDepth or 0) + 1,
846 | },
847 | }),
848 | element:clone({
849 | startIndex = back_startIndex,
850 | endIndex = back_endIndex,
851 | kind = "back",
852 | value = table.concat(back_lines),
853 | extraProps = {
854 | listStyle = list_type,
855 | listDepth = (element.extraProps.listDepth or 0) + 1,
856 | },
857 | }),
858 | }
859 | end
860 |
861 | ---@param backend PodiumBackend | string
862 | ---@param source string
863 | ---@return string
864 | local function process(backend, source)
865 | backend = type(backend) == "string" and M[backend] or backend ---@cast backend PodiumBackend
866 | local elements = splitParagraphs(PodiumElement.new(source))
867 | local nl = guessNewline(source)
868 | local shouldProcess = false
869 | local i = 1
870 | while i <= #elements do
871 | local element = elements[i]
872 | if element.kind == "pod" then
873 | shouldProcess = true
874 | end
875 | if shouldProcess then
876 | if element.kind == "text" or element.kind == "backspace" then
877 | i = i + 1
878 | else
879 | if element.source == nil then
880 | error("element.source is nil")
881 | end
882 | if type(element) ~= "table" then
883 | error("element is not a table")
884 | end
885 | elements = append(
886 | slice(elements, 1, i - 1),
887 | backend.rules[element.kind](element),
888 | slice(elements, i + 1)
889 | )
890 | end
891 | else
892 | elements = append(slice(elements, 1, i - 1), slice(elements, i + 1))
893 | end
894 | if element.kind == "cut" then
895 | shouldProcess = false
896 | end
897 | end
898 | elements = append(
899 | backend.rules["preamble"](PodiumElement.new(source)),
900 | elements,
901 | backend.rules["postamble"](PodiumElement.new(source))
902 | )
903 | local output = ""
904 | for _, element in ipairs(elements) do
905 | if element.kind == "backspace" then
906 | local count = element.extraProps.deleteCount or 1
907 | output = output:sub(1, #output - count)
908 | else
909 | local text = element.value:gsub(nl, nl .. (" "):rep(element.indentLevel))
910 | output = output .. text
911 | end
912 | end
913 | return output
914 | end
915 |
916 | ---@alias PodiumBackendRule fun(element: PodiumElement): PodiumElement[]
917 | ---@class PodiumBackend
918 | ---@field rules table
919 | ---@field registerSimpleFormattingCode fun(self: PodiumBackend, name: string, fun: fun(content: string): string): PodiumBackend
920 | ---@field registerSimpleCommand fun(self: PodiumBackend, name: string, fun: fun(content: string): string): PodiumBackend
921 | ---@field registerSimpleDataParagraph fun(self: PodiumBackend, name: string, fun: fun(content: string): string): PodiumBackend
922 | ---@field registerSimple fun(self: PodiumBackend, name: string, fun: fun(content: string): string): PodiumBackend
923 | ---@field register fun(self: PodiumBackend, name: string, fun: PodiumBackendRule): PodiumBackend
924 | local PodiumBackend = {
925 | rules = {},
926 | }
927 |
928 | ---@param self PodiumBackend
929 | ---@param name string
930 | ---@param fun fun(content: string): string
931 | function PodiumBackend.registerSimpleFormattingCode(self, name, fun)
932 | self = type(self) == "string" and M[self] or self ---@cast self PodiumBackend
933 | self.rules[name] = function(element)
934 | local _, b_arg, e_arg, _ = findFormattingCode(element)
935 | local arg = element.source:sub(b_arg, e_arg)
936 | return {
937 | element:clone({ kind = "text", value = fun(arg) }),
938 | }
939 | end
940 | return self
941 | end
942 |
943 | ---@param self PodiumBackend | string
944 | ---@param name string
945 | ---@param fun fun(content: string): string
946 | ---@return PodiumBackend
947 | function PodiumBackend.registerSimpleCommand(self, name, fun)
948 | self = type(self) == "string" and M[self] or self ---@cast self PodiumBackend
949 | self.rules[name] = function(element)
950 | local arg =
951 | element.source:sub(element.startIndex, element.endIndex):gsub("^=%S+", "")
952 | return {
953 | element:clone({ kind = "text", value = fun(arg) }),
954 | }
955 | end
956 | return self
957 | end
958 |
959 | ---@param element PodiumElement
960 | ---@return integer, integer, integer, integer
961 | ---The index of the first character of =begin command
962 | ---The index of the first character of content
963 | ---The index of the last character of content
964 | ---The index of the last character of =end command
965 | local function findDataParagraph(element)
966 | local startIndex = element.startIndex
967 | local endIndex = element.startIndex
968 | local lines = {}
969 | local blockState = 0
970 | for _, line in ipairs(splitLines(element)) do
971 | if blockState == 0 then
972 | startIndex = startIndex + #line
973 | endIndex = endIndex + #line
974 | if line:match("^=begin") then
975 | blockState = 1
976 | end
977 | elseif blockState == 1 then
978 | startIndex = startIndex + #line
979 | endIndex = endIndex + #line
980 | if line:match("^%s*$") then
981 | startIndex = startIndex - #line
982 | table.insert(lines, line)
983 | blockState = 2
984 | end
985 | elseif blockState == 2 then
986 | endIndex = endIndex + #line
987 | if line:match("^%s*$") then
988 | table.insert(lines, line)
989 | blockState = 3
990 | else
991 | table.insert(lines, line)
992 | end
993 | elseif blockState == 3 then
994 | endIndex = endIndex + #line
995 | if line:match("^=end") then
996 | endIndex = endIndex - #line
997 | blockState = 4
998 | elseif line:match("^%s*$") then
999 | table.insert(lines, line)
1000 | blockState = 3
1001 | else
1002 | table.insert(lines, line)
1003 | blockState = 2
1004 | end
1005 | end
1006 | end
1007 | if blockState ~= 4 then
1008 | error("Data paragraph is unexpectedly terminated")
1009 | end
1010 | return element.startIndex, startIndex, endIndex - 1, element.endIndex
1011 | end
1012 |
1013 | ---@param self PodiumBackend
1014 | ---@param name string
1015 | ---@param fun fun(content: string): string
1016 | ---@return PodiumBackend
1017 | function PodiumBackend.registerSimpleDataParagraph(self, name, fun)
1018 | self = type(self) == "string" and M[self] or self ---@cast self PodiumBackend
1019 | self.rules[name] = function(element)
1020 | return {
1021 | element:clone({ kind = "text", value = fun(element.value) }),
1022 | }
1023 | end
1024 | return self
1025 | end
1026 |
1027 | ---@param self PodiumBackend
1028 | ---@param name string
1029 | ---@param fun fun(content: string): string
1030 | ---@return PodiumBackend
1031 | function PodiumBackend.registerSimple(self, name, fun)
1032 | self = type(self) == "string" and M[self] or self ---@cast self PodiumBackend
1033 | self.rules[name] = function(element)
1034 | return {
1035 | element:clone({ kind = "text", value = fun(element.value) }),
1036 | }
1037 | end
1038 | return self
1039 | end
1040 |
1041 | ---@param self PodiumBackend
1042 | ---@param name string
1043 | ---@param fun fun(content: string): string
1044 | ---@return PodiumBackend
1045 | function PodiumBackend.register(self, name, fun)
1046 | self = type(self) == "string" and M[self] or self ---@cast self PodiumBackend
1047 | self.rules[name] = fun
1048 | return self
1049 | end
1050 |
1051 | ---@param rules table
1052 | ---@return PodiumBackend
1053 | function PodiumBackend.new(rules)
1054 | setmetatable(rules, {
1055 | __index = function(_table, _key)
1056 | return function(_element)
1057 | return {}
1058 | end
1059 | end,
1060 | })
1061 | local self = setmetatable({
1062 | rules = rules,
1063 | }, {
1064 | __index = PodiumBackend,
1065 | })
1066 | return self
1067 | end
1068 |
1069 | local html = PodiumBackend.new({
1070 | preamble = function(element)
1071 | return {}
1072 | end,
1073 | postamble = function(element)
1074 | return {}
1075 | end,
1076 | head1 = function(element)
1077 | local nl = guessNewline(element.source)
1078 | return append(
1079 | { element:clone({ value = "", kind = "text" }) },
1080 | splitTokens(element:sub((element:find("%s"))):trim(), true),
1081 | { element:clone({ value = " " .. nl, kind = "text" }) }
1082 | )
1083 | end,
1084 | head2 = function(element)
1085 | local nl = guessNewline(element.source)
1086 | return append(
1087 | { element:clone({ value = "", kind = "text" }) },
1088 | splitTokens(element:sub((element:find("%s"))):trim(), true),
1089 | { element:clone({ value = " " .. nl, kind = "text" }) }
1090 | )
1091 | end,
1092 | head3 = function(element)
1093 | local nl = guessNewline(element.source)
1094 | return append(
1095 | { element:clone({ value = "", kind = "text" }) },
1096 | splitTokens(element:sub((element:find("%s"))):trim(), true),
1097 | { element:clone({ value = " " .. nl, kind = "text" }) }
1098 | )
1099 | end,
1100 | head4 = function(element)
1101 | local nl = guessNewline(element.source)
1102 | return append(
1103 | { element:clone({ value = "", kind = "text" }) },
1104 | splitTokens(element:sub((element:find("%s"))):trim(), true),
1105 | { element:clone({ value = " " .. nl, kind = "text" }) }
1106 | )
1107 | end,
1108 | paragraph = function(element)
1109 | local nl = guessNewline(element.source)
1110 | return append(
1111 | { element:clone({ value = "", kind = "text" }) },
1112 | splitTokens(element:trim(), true),
1113 | { element:clone({ value = "
" .. nl, kind = "text" }) }
1114 | )
1115 | end,
1116 | over = function(element)
1117 | local nl = guessNewline(element.source)
1118 | if element.extraProps.listStyle == "ordered" then
1119 | return {
1120 | element:clone({ value = "" .. nl, kind = "text" }),
1121 | }
1122 | else
1123 | return {
1124 | element:clone({ value = "" .. nl, kind = "text" }),
1125 | }
1126 | end
1127 | end,
1128 | back = function(element)
1129 | local ld = element.extraProps.listDepth
1130 | local nl = guessNewline(element.source)
1131 | if element.extraProps.listStyle == "ordered" then
1132 | return {
1133 | element:clone({ value = " ", kind = "text" }),
1134 | element:clone({ value = (ld == 1 and nl or ""), kind = "text" }),
1135 | }
1136 | else
1137 | return {
1138 | element:clone({ value = "", kind = "text" }),
1139 | element:clone({ value = (ld == 1 and nl or ""), kind = "text" }),
1140 | }
1141 | end
1142 | end,
1143 | cut = function(element)
1144 | return {}
1145 | end,
1146 | pod = function(element)
1147 | return {}
1148 | end,
1149 | verbatim = function(element)
1150 | local nl = guessNewline(element.source)
1151 | return {
1152 | element:clone({ value = "", kind = "text" }),
1153 | element:sanitize():clone({ kind = "text" }),
1154 | element:clone({ value = " " .. nl, kind = "text" }),
1155 | }
1156 | end,
1157 | data = function(element)
1158 | local _, startIndex, endIndex, _ = findDataParagraph(element)
1159 | local dataKind = element.extraProps.dataKind
1160 | return {
1161 | element:sub(startIndex, endIndex):clone({ kind = dataKind }),
1162 | }
1163 | end,
1164 | ["for"] = function(element)
1165 | local nl = guessNewline(element.source)
1166 | local _, startIndex, dataKind = element:find("^=for%s+(%S+)%s")
1167 | return { element:sub(startIndex):clone({ kind = dataKind }) }
1168 | end,
1169 | html = function(element)
1170 | return { element:clone({ kind = "text" }) }
1171 | end,
1172 | item = function(element)
1173 | local nl = guessNewline(element.source)
1174 | local _, startIndex = element:find("^=item%s*[*0-9]*%.?.")
1175 | return append(
1176 | { element:clone({ kind = "text", value = "" }) },
1177 | splitItem(element:sub(startIndex):trim()),
1178 | { element:clone({ kind = "text", value = " " .. nl }) }
1179 | )
1180 | end,
1181 | list = function(element)
1182 | return splitList(element)
1183 | end,
1184 | items = function(element)
1185 | return splitItems(element)
1186 | end,
1187 | itempart = function(element)
1188 | return splitTokens(element, true)
1189 | end,
1190 | I = function(element)
1191 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1192 | return append(
1193 | { element:clone({ value = "", kind = "text" }) },
1194 | splitTokens(element:sub(startIndex, endIndex):trim(), true),
1195 | { element:clone({ value = " ", kind = "text" }) }
1196 | )
1197 | end,
1198 | B = function(element)
1199 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1200 | return append(
1201 | { element:clone({ value = "", kind = "text" }) },
1202 | splitTokens(element:sub(startIndex, endIndex):trim(), true),
1203 | { element:clone({ value = " ", kind = "text" }) }
1204 | )
1205 | end,
1206 | C = function(element)
1207 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1208 | return append(
1209 | { element:clone({ value = "", kind = "text" }) },
1210 | splitTokens(element:sub(startIndex, endIndex):trim(), true),
1211 | { element:clone({ value = "", kind = "text" }) }
1212 | )
1213 | end,
1214 | L = function(element)
1215 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1216 | local newElement = element:sub(startIndex, endIndex)
1217 | local b, e = newElement:find("[^|]*|")
1218 | if b then
1219 | return append(
1220 | { element:clone({ value = ' 0 then
1461 | return append(tokens, {
1462 | element:clone({
1463 | kind = "text",
1464 | value = "~" .. nl .. string.rep(" ", padding),
1465 | }),
1466 | }, tags, { element:clone({ kind = "text", value = nl .. nl }) })
1467 | else
1468 | return append(
1469 | tokens,
1470 | { element:clone({ kind = "text", value = "~" .. nl .. nl }) }
1471 | )
1472 | end
1473 | end
1474 |
1475 | local vimdoc = PodiumBackend.new({
1476 | preamble = function(element)
1477 | local nl = guessNewline(element.source)
1478 | local frontmatter = parseFrontMatter(element.source)
1479 | local filename = "*" .. (frontmatter.name or "untitled") .. ".txt*"
1480 | local description = frontmatter.description or "No description"
1481 | local spaces = string.rep(" ", 78 - #filename - #description - #nl)
1482 | return {
1483 | element:clone({
1484 | kind = "text",
1485 | value = filename .. spaces .. description .. nl,
1486 | }),
1487 | }
1488 | end,
1489 | postamble = function(element)
1490 | local nl = guessNewline(element.source)
1491 | return {
1492 | element:clone({
1493 | kind = "text",
1494 | value = nl .. "vim:tw=78:ts=8:noet:ft=help:norl:" .. nl,
1495 | }),
1496 | }
1497 | end,
1498 | head1 = function(element)
1499 | local nl = guessNewline(element.source)
1500 | return append({
1501 | element:clone({
1502 | kind = "text",
1503 | value = string.rep("=", 78 - #nl) .. nl,
1504 | }),
1505 | }, vimdoc_head(element))
1506 | end,
1507 | head2 = vimdoc_head,
1508 | head3 = vimdoc_head,
1509 | head4 = vimdoc_head,
1510 | paragraph = function(element)
1511 | local nl = guessNewline(element.source)
1512 | return append(splitTokens(element:trim()), {
1513 | element:clone({ kind = "text", value = nl .. nl }),
1514 | })
1515 | end,
1516 | over = function(element)
1517 | local nl = guessNewline(element.source)
1518 | if element.extraProps.listDepth == 1 then
1519 | return {
1520 | element:clone({ kind = "backspace", extraProps = { deleteCount = 1 } }),
1521 | element:clone({ kind = "text", value = nl }),
1522 | }
1523 | else
1524 | return {
1525 | element:clone({ kind = "text", value = nl }),
1526 | }
1527 | end
1528 | end,
1529 | back = function(element)
1530 | local nl = guessNewline(element.source)
1531 | if element.extraProps.listDepth == 1 then
1532 | return { element:clone({ kind = "text", value = nl }) }
1533 | else
1534 | return {}
1535 | end
1536 | end,
1537 | cut = function(element)
1538 | return {}
1539 | end,
1540 | pod = function(element)
1541 | return {}
1542 | end,
1543 | verbatim = function(element)
1544 | local nl = guessNewline(element.source)
1545 | return {
1546 | element:clone({ kind = "text", value = ">" .. nl }),
1547 | element:clone({ kind = "text" }),
1548 | element:clone({ kind = "backspace", extraProps = { deleteCount = 1 } }),
1549 | element:clone({ kind = "text", value = "<" .. nl .. nl }),
1550 | }
1551 | end,
1552 | data = function(element)
1553 | local _, startIndex, endIndex, _ = findDataParagraph(element)
1554 | local dataKind = element.extraProps.dataKind
1555 | return {
1556 | element:sub(startIndex, endIndex):clone({ kind = dataKind }),
1557 | }
1558 | end,
1559 | ["for"] = function(element)
1560 | local nl = guessNewline(element.source)
1561 | local _, startIndex, dataKind = element:find("^=for%s+(%S+)%s")
1562 | return { element:sub(startIndex):clone({ kind = dataKind }) }
1563 | end,
1564 | vimdoc = function(element)
1565 | return { element:clone({ kind = "text" }) }
1566 | end,
1567 | item = function(element)
1568 | local nl = guessNewline(element.source)
1569 | local bullet = "-"
1570 | if element.source:sub(1, element.endIndex):match("^=item%s*[0-9]") then
1571 | _, _, bullet = element:find("^=item%s*([0-9]+%.?)")
1572 | end
1573 | local _, startIndex = element:find("^=item%s*[*0-9]*%.?.")
1574 | return append(
1575 | { element:clone({ kind = "text", value = bullet .. " " }) },
1576 | splitItem(element:sub(startIndex):trim()),
1577 | { element:clone({ kind = "text", value = nl }) }
1578 | )
1579 | end,
1580 | list = function(element)
1581 | return splitList(element)
1582 | end,
1583 | items = function(element)
1584 | return splitItems(element)
1585 | end,
1586 | itempart = function(element)
1587 | return splitTokens(element)
1588 | end,
1589 | B = function(element)
1590 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1591 | return append(
1592 | { element:clone({ kind = "text", value = "{" }) },
1593 | splitTokens(element:sub(startIndex, endIndex):trim()),
1594 | { element:clone({ kind = "text", value = "}" }) }
1595 | )
1596 | end,
1597 | C = function(element)
1598 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1599 | return append(
1600 | { element:clone({ kind = "text", value = "`" }) },
1601 | splitTokens(element:sub(startIndex, endIndex):trim()),
1602 | { element:clone({ kind = "text", value = "`" }) }
1603 | )
1604 | end,
1605 | O = function(element)
1606 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1607 | return append(
1608 | { element:clone({ kind = "text", value = "'" }) },
1609 | splitTokens(element:sub(startIndex, endIndex):trim()),
1610 | { element:clone({ kind = "text", value = "'" }) }
1611 | )
1612 | end,
1613 | L = function(element)
1614 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1615 | local newElement = element:sub(startIndex, endIndex):trim()
1616 | local b, e = newElement:find("[^|]*|")
1617 | if b then
1618 | return append(
1619 | splitTokens(newElement:sub(b, e - 1)),
1620 | { element:clone({ kind = "text", value = " |" }) },
1621 | splitTokens(newElement:sub(e + 1)),
1622 | { element:clone({ kind = "text", value = "|" }) }
1623 | )
1624 | else
1625 | return append(
1626 | { element:clone({ kind = "text", value = "|" }) },
1627 | splitTokens(newElement),
1628 | { element:clone({ kind = "text", value = "|" }) }
1629 | )
1630 | end
1631 | end,
1632 | X = function(element)
1633 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1634 | return append(
1635 | { element:clone({ kind = "text", value = "*" }) },
1636 | splitTokens(element:sub(startIndex, endIndex):trim()),
1637 | { element:clone({ kind = "text", value = "*" }) }
1638 | )
1639 | end,
1640 | E = function(element)
1641 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1642 | local value = element:sub(startIndex, endIndex):trim().value
1643 | if value == "lt" then
1644 | return { element:clone({ kind = "text", value = "<" }) }
1645 | elseif value == "gt" then
1646 | return { element:clone({ kind = "text", value = ">" }) }
1647 | elseif value == "verbar" then
1648 | return { element:clone({ kind = "text", value = "|" }) }
1649 | elseif value == "sol" then
1650 | return { element:clone({ kind = "text", value = "/" }) }
1651 | else
1652 | return {
1653 | element:clone({ kind = "text", value = "&" .. value .. ";" }),
1654 | }
1655 | end
1656 | end,
1657 | Z = function(element)
1658 | return {}
1659 | end,
1660 | })
1661 |
1662 | local latex = PodiumBackend.new({
1663 | preamble = function(element)
1664 | return {}
1665 | end,
1666 | postamble = function(element)
1667 | return {}
1668 | end,
1669 | head1 = function(element)
1670 | local nl = guessNewline(element.source)
1671 | return append(
1672 | { element:clone({ kind = "text", value = "\\section{" }) },
1673 | splitTokens(element:sub((element:find("%s"))):trim()),
1674 | { element:clone({ kind = "text", value = "}" .. nl }) }
1675 | )
1676 | end,
1677 | head2 = function(element)
1678 | local nl = guessNewline(element.source)
1679 | return append(
1680 | { element:clone({ kind = "text", value = "\\subsection{" }) },
1681 | splitTokens(element:sub((element:find("%s"))):trim()),
1682 | { element:clone({ kind = "text", value = "}" .. nl }) }
1683 | )
1684 | end,
1685 | head3 = function(element)
1686 | local nl = guessNewline(element.source)
1687 | return append(
1688 | { element:clone({ kind = "text", value = "\\subsubsection{" }) },
1689 | splitTokens(element:sub((element:find("%s"))):trim()),
1690 | { element:clone({ kind = "text", value = "}" .. nl }) }
1691 | )
1692 | end,
1693 | paragraph = function(element)
1694 | local nl = guessNewline(element.source)
1695 | return append(
1696 | splitTokens(element:trim()),
1697 | { element:clone({ kind = "text", value = nl }) }
1698 | )
1699 | end,
1700 | over = function(element)
1701 | local nl = guessNewline(element.source)
1702 | if element.extraProps.listStyle == "ordered" then
1703 | return {
1704 | element:clone({ kind = "text", value = "\\begin{enumerate}" .. nl }),
1705 | }
1706 | else
1707 | return {
1708 | element:clone({ kind = "text", value = "\\begin{itemize}" .. nl }),
1709 | }
1710 | end
1711 | end,
1712 | back = function(element)
1713 | local ld = element.extraProps.listDepth
1714 | local nl = guessNewline(element.source)
1715 | if element.extraProps.listStyle == "ordered" then
1716 | return {
1717 | element:clone({ kind = "text", value = "\\end{enumerate}" }),
1718 | element:clone({ kind = "text", value = ld == 1 and nl or "" }),
1719 | }
1720 | else
1721 | return {
1722 | element:clone({ kind = "text", value = "\\end{itemize}" }),
1723 | element:clone({ kind = "text", value = ld == 1 and nl or "" }),
1724 | }
1725 | end
1726 | end,
1727 | cut = function(element)
1728 | return {}
1729 | end,
1730 | pod = function(element)
1731 | return {}
1732 | end,
1733 | verbatim = function(element)
1734 | local nl = guessNewline(element.source)
1735 | return {
1736 | element:clone({ kind = "text", value = "\\begin{verbatim}" .. nl }),
1737 | element:clone({ kind = "text" }),
1738 | element:clone({ kind = "backspace", extraProps = { deleteCount = 1 } }),
1739 | element:clone({ kind = "text", value = "\\end{verbatim}" .. nl }),
1740 | }
1741 | end,
1742 | data = function(element)
1743 | local _, startIndex, endIndex, _ = findDataParagraph(element)
1744 | local dataKind = element.extraProps.dataKind
1745 | return {
1746 | element:sub(startIndex, endIndex):clone({ kind = dataKind }),
1747 | }
1748 | end,
1749 | ["for"] = function(element)
1750 | local nl = guessNewline(element.source)
1751 | local _, startIndex, dataKind = element:find("^=for%s+(%S+)%s")
1752 | return { element:sub(startIndex):clone({ kind = dataKind }) }
1753 | end,
1754 | latex = function(element)
1755 | return { element:clone({ kind = "text" }) }
1756 | end,
1757 | item = function(element)
1758 | local nl = guessNewline(element.source)
1759 | local _, startIndex = element:find("^=item%s*[*0-9]*%.?.")
1760 | return append(
1761 | { element:clone({ kind = "text", value = "\\item " }) },
1762 | splitItem(element:sub(startIndex):trim()),
1763 | { element:clone({ kind = "text", value = nl }) }
1764 | )
1765 | end,
1766 | list = function(element)
1767 | return splitList(element)
1768 | end,
1769 | items = function(element)
1770 | return splitItems(element)
1771 | end,
1772 | itempart = function(element)
1773 | return splitTokens(element)
1774 | end,
1775 | I = function(element)
1776 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1777 | return append(
1778 | { element:clone({ value = "\\textit{", kind = "text" }) },
1779 | splitTokens(element:sub(startIndex, endIndex):trim()),
1780 | { element:clone({ value = "}", kind = "text" }) }
1781 | )
1782 | end,
1783 | B = function(element)
1784 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1785 | return append(
1786 | { element:clone({ value = "\\textbf{", kind = "text" }) },
1787 | splitTokens(element:sub(startIndex, endIndex):trim()),
1788 | { element:clone({ value = "}", kind = "text" }) }
1789 | )
1790 | end,
1791 | C = function(element)
1792 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1793 | return append(
1794 | { element:clone({ value = "\\verb|", kind = "text" }) },
1795 | splitTokens(element:sub(startIndex, endIndex):trim()),
1796 | { element:clone({ value = "|", kind = "text" }) }
1797 | )
1798 | end,
1799 | L = function(element)
1800 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1801 | local newElement = element:sub(startIndex, endIndex):trim()
1802 | local b, e = newElement:find("[^|]*|")
1803 | if b then
1804 | return append(
1805 | { element:clone({ value = "\\href{", kind = "text" }) },
1806 | splitTokens(newElement:sub(e + 1)),
1807 | { element:clone({ value = "}{", kind = "text" }) },
1808 | splitTokens(newElement:sub(b, e - 1)),
1809 | { element:clone({ value = "}", kind = "text" }) }
1810 | )
1811 | elseif element.value:match("^https?://") then
1812 | return append(
1813 | { element:clone({ value = "\\url{", kind = "text" }) },
1814 | splitTokens(newElement),
1815 | { element:clone({ value = "}", kind = "text" }) }
1816 | )
1817 | else
1818 | return append(
1819 | { element:clone({ value = "\\ref{", kind = "text" }) },
1820 | splitTokens(newElement),
1821 | { element:clone({ value = "}", kind = "text" }) }
1822 | )
1823 | end
1824 | end,
1825 | E = function(element)
1826 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1827 | local value = element:sub(startIndex, endIndex):trim().value
1828 | if value == "lt" then
1829 | return { element:clone({ value = "<", kind = "text" }) }
1830 | elseif value == "gt" then
1831 | return { element:clone({ value = ">", kind = "text" }) }
1832 | elseif value == "verbar" then
1833 | return { element:clone({ value = "|", kind = "text" }) }
1834 | elseif value == "sol" then
1835 | return { element:clone({ value = "/", kind = "text" }) }
1836 | else
1837 | return {
1838 | element:clone({ value = "\\texttt{", kind = "text" }),
1839 | element:clone({ value = value }),
1840 | element:clone({ value = "}", kind = "text" }),
1841 | }
1842 | end
1843 | end,
1844 | X = function(element)
1845 | local _, startIndex, endIndex, _ = findFormattingCode(element)
1846 | return append(
1847 | { element:clone({ value = "\\label{", kind = "text" }) },
1848 | splitTokens(element:sub(startIndex, endIndex):trim()),
1849 | { element:clone({ value = "}", kind = "text" }) }
1850 | )
1851 | end,
1852 | Z = function(element)
1853 | return {}
1854 | end,
1855 | })
1856 |
1857 | M.PodiumElement = PodiumElement
1858 | M.PodiumBackend = PodiumBackend
1859 | M.unindent = unindent
1860 | M.append = append
1861 | M.slice = slice
1862 | M.guessNewline = guessNewline
1863 | M.indexToRowCol = indexToRowCol
1864 | M.splitLines = splitLines
1865 | M.splitParagraphs = splitParagraphs
1866 | M.splitItem = splitItem
1867 | M.splitItems = splitItems
1868 | M.findFormattingCode = findFormattingCode
1869 | M.findDataParagraph = findDataParagraph
1870 | M.splitTokens = splitTokens
1871 | M.splitList = splitList
1872 | M.html = html
1873 | M.markdown = markdown
1874 | M.vimdoc = vimdoc
1875 | M.latex = latex
1876 | M.process = process
1877 |
1878 | if arg then
1879 | if #arg > 0 and arg[0]:match("podium") then
1880 | local input
1881 | if arg[2] then
1882 | local ifile = io.open(arg[2], "r")
1883 | if not ifile then
1884 | error("cannot open file: " .. arg[2])
1885 | end
1886 | input = ifile:read("*a")
1887 | else
1888 | input = io.read("*a")
1889 | end
1890 | local output = process(M[arg[1]], input)
1891 | if arg[3] then
1892 | local ofile = io.open(arg[3], "w")
1893 | if not ofile then
1894 | error("cannot open file: " .. arg[3])
1895 | end
1896 | ofile:write(output)
1897 | else
1898 | io.write(output)
1899 | end
1900 | end
1901 | end
1902 |
1903 | return M
1904 |
--------------------------------------------------------------------------------