├── 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 | 68 | 71 |
72 |
73 | 74 | 75 |
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 | --------------------------------------------------------------------------------