├── example.md ├── default.nix ├── LICENSE ├── README.md ├── .gitignore └── slack.lua /example.md: -------------------------------------------------------------------------------- 1 | # This is a first heading. 2 | 3 | ## This is a second heading. 4 | 5 | This is a list: 6 | 7 | 1. Item 1. 8 | 2. Item 2. 9 | 3. Item 3. 10 | 11 | _this text should be emphasized_ 12 | 13 | ```python 14 | import this 15 | print("this is a code block") 16 | ``` 17 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | stdenv.mkDerivation rec { 3 | name = "md2slack"; 4 | src = ./.; 5 | env = buildEnv { name = name; paths = buildInputs; }; 6 | buildInputs = [ 7 | pandoc 8 | which 9 | ]; 10 | installPhase = '' 11 | mkdir -p "$out/bin" 12 | cp slack.lua $out/ 13 | echo "#! ${stdenv.shell}" >> "$out/bin/md2slack" 14 | echo "exec $(which pandoc) -f gfm -t $out/slack.lua" >> "$out/bin/md2slack" 15 | chmod a+x "$out/bin/md2slack" 16 | ''; 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Omar Bohsali 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pandoc-md-to-slack 2 | 3 | Pandoc extension to convert markdown to slack formatting. More here: [http://omarish.com/2018/06/24/convert-markdown-to-slack-formatting.html](http://omarish.com/2018/06/24/convert-markdown-to-slack-formatting.html). 4 | 5 | ## How? 6 | 7 | ```bash 8 | $ pandoc -f gfm -t slack.lua example.md 9 | ``` 10 | 11 | ## Example 12 | 13 | ``` 14 | › cat example.md 15 | # This is a first heading. 16 | 17 | ## This is a second heading. 18 | 19 | This is a list: 20 | 21 | 1. Item 1. 22 | 2. Item 2. 23 | 3. Item 3. 24 | 25 | _this text should be emphasized_ 26 | ``` 27 | 28 | Convert to slack: 29 | 30 | ```bash 31 | $ pandoc -f gfm -t slack.lua example.md 32 | ``` 33 | 34 | Result: 35 | 36 | ``` 37 | *This is a first heading.* 38 | 39 | *This is a second heading.* 40 | 41 | This is a list: 42 | 43 | * Item 1. 44 | * Item 2. 45 | * Item 3. 46 | 47 | _this text should be emphasized_ 48 | ``` 49 | 50 | ## Installing with nix 51 | 52 | If you use the [Nix](https://nixos.org/nix/) package manager you can install this tool as follows: 53 | 54 | ``` 55 | $ nix-env -f . -i 56 | ... 57 | $ echo "Hello *world*." | md2slack 58 | Hello _world_. 59 | 60 | $ 61 | ``` 62 | 63 | It will install a script named `md2slack` that accepts input via stdin. 64 | 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Nix 107 | result 108 | -------------------------------------------------------------------------------- /slack.lua: -------------------------------------------------------------------------------- 1 | local function escape(s, in_attribute) 2 | return s:gsub("[<>&\"']", 3 | function(x) 4 | if x == '<' then 5 | return '<' 6 | elseif x == '>' then 7 | return '>' 8 | elseif x == '&' then 9 | return '&' 10 | elseif x == '"' then 11 | return '"' 12 | elseif x == "'" then 13 | return ''' 14 | else 15 | return x 16 | end 17 | end) 18 | end 19 | 20 | -- Helper function to convert an attributes table into 21 | -- a string that can be put into HTML tags. 22 | local function attributes(attr) 23 | local attr_table = {} 24 | for x,y in pairs(attr) do 25 | if y and y ~= "" then 26 | table.insert(attr_table, ' ' .. x .. '="' .. escape(y,true) .. '"') 27 | end 28 | end 29 | return table.concat(attr_table) 30 | end 31 | 32 | -- Run cmd on a temporary file containing inp and return result. 33 | local function pipe(cmd, inp) 34 | local tmp = os.tmpname() 35 | local tmph = io.open(tmp, "w") 36 | tmph:write(inp) 37 | tmph:close() 38 | local outh = io.popen(cmd .. " " .. tmp,"r") 39 | local result = outh:read("*all") 40 | outh:close() 41 | os.remove(tmp) 42 | return result 43 | end 44 | 45 | -- Table to store footnotes, so they can be included at the end. 46 | local notes = {} 47 | 48 | -- Blocksep is used to separate block elements. 49 | function Blocksep() 50 | return "\n\n" 51 | end 52 | 53 | -- This function is called once for the whole document. Parameters: 54 | -- body is a string, metadata is a table, variables is a table. 55 | -- This gives you a fragment. You could use the metadata table to 56 | -- fill variables in a custom lua template. Or, pass `--template=...` 57 | -- to pandoc, and pandoc will add do the template processing as 58 | -- usual. 59 | function Doc(body, metadata, variables) 60 | local buffer = {} 61 | local function add(s) 62 | table.insert(buffer, s) 63 | end 64 | add(body) 65 | if #notes > 0 then 66 | add('
    ') 67 | for _,note in pairs(notes) do 68 | add(note) 69 | end 70 | add('
') 71 | end 72 | return table.concat(buffer,'\n') 73 | end 74 | 75 | -- The functions that follow render corresponding pandoc elements. 76 | -- s is always a string, attr is always a table of attributes, and 77 | -- items is always an array of strings (the items in a list). 78 | -- Comments indicate the types of other variables. 79 | 80 | function Str(s) 81 | return escape(s) 82 | end 83 | 84 | function Space() 85 | return " " 86 | end 87 | 88 | function Subscript(s) 89 | return "" .. s .. "" 90 | end 91 | 92 | function Superscript(s) 93 | return "" .. s .. "" 94 | end 95 | 96 | function SmallCaps(s) 97 | return '' .. s .. '' 98 | end 99 | 100 | function Link(s, src, tit) 101 | return "" .. s .. "" 103 | end 104 | 105 | function Image(s, src, tit) 106 | return "" 108 | end 109 | 110 | function Code(s, attr) 111 | return "" .. escape(s) .. "" 112 | end 113 | 114 | function InlineMath(s) 115 | return "\\(" .. escape(s) .. "\\)" 116 | end 117 | 118 | function DisplayMath(s) 119 | return "\\[" .. escape(s) .. "\\]" 120 | end 121 | 122 | function Note(s) 123 | local num = #notes + 1 124 | -- insert the back reference right before the final closing tag. 125 | s = string.gsub(s, 126 | '(.*)' .. s .. '') 129 | -- return the footnote reference, linked to the note. 130 | return '' .. num .. '' 132 | end 133 | 134 | function Span(s, attr) 135 | return "" .. s .. "" 136 | end 137 | 138 | function Cite(s, cs) 139 | local ids = {} 140 | for _,cit in ipairs(cs) do 141 | table.insert(ids, cit.citationId) 142 | end 143 | return "" .. s .. "" 145 | end 146 | 147 | function Plain(s) 148 | return s 149 | end 150 | 151 | function Para(s) 152 | return s .. "\n" 153 | end 154 | 155 | -- lev is an integer, the header level. 156 | function Header(lev, s, attr) 157 | return "" .. s .. "" 158 | end 159 | 160 | function BlockQuote(s) 161 | return "
\n" .. s .. "\n
" 162 | end 163 | 164 | function HorizontalRule() 165 | return "
" 166 | end 167 | 168 | function CodeBlock(s, attr) 169 | -- If code block has class 'dot', pipe the contents through dot 170 | -- and base64, and include the base64-encoded png as a data: URL. 171 | if attr.class and string.match(' ' .. attr.class .. ' ',' dot ') then 172 | local png = pipe("base64", pipe("dot -Tpng", s)) 173 | return '' 174 | -- otherwise treat as code (one could pipe through a highlighter) 175 | else 176 | return "```\n" .. s .. "```\n" 177 | end 178 | end 179 | 180 | function OrderedList(items) 181 | local buffer = {} 182 | for _, item in pairs(items) do 183 | table.insert(buffer, "* " .. item .. "") 184 | end 185 | return table.concat(buffer, "\n") 186 | end 187 | 188 | -- Revisit association list STackValue instance. 189 | function DefinitionList(items) 190 | local buffer = {} 191 | for _,item in pairs(items) do 192 | for k, v in pairs(item) do 193 | table.insert(buffer,"
" .. k .. "
\n
" .. 194 | table.concat(v,"
\n
") .. "
") 195 | end 196 | end 197 | return "
\n" .. table.concat(buffer, "\n") .. "\n
" 198 | end 199 | 200 | -- Convert pandoc alignment to something HTML can use. 201 | -- align is AlignLeft, AlignRight, AlignCenter, or AlignDefault. 202 | function html_align(align) 203 | if align == 'AlignLeft' then 204 | return 'left' 205 | elseif align == 'AlignRight' then 206 | return 'right' 207 | elseif align == 'AlignCenter' then 208 | return 'center' 209 | else 210 | return 'left' 211 | end 212 | end 213 | 214 | -- Caption is a string, aligns is an array of strings, 215 | -- widths is an array of floats, headers is an array of 216 | -- strings, rows is an array of arrays of strings. 217 | function Table(caption, aligns, widths, headers, rows) 218 | local buffer = {} 219 | local function add(s) 220 | table.insert(buffer, s) 221 | end 222 | add("") 223 | if caption ~= "" then 224 | add("") 225 | end 226 | if widths and widths[1] ~= 0 then 227 | for _, w in pairs(widths) do 228 | add('') 229 | end 230 | end 231 | local header_row = {} 232 | local empty_header = true 233 | for i, h in pairs(headers) do 234 | local align = html_align(aligns[i]) 235 | table.insert(header_row,'') 236 | empty_header = empty_header and h == "" 237 | end 238 | if empty_header then 239 | head = "" 240 | else 241 | add('') 242 | for _,h in pairs(header_row) do 243 | add(h) 244 | end 245 | add('') 246 | end 247 | local class = "even" 248 | for _, row in pairs(rows) do 249 | class = (class == "even" and "odd") or "even" 250 | add('') 251 | for i,c in pairs(row) do 252 | add('') 253 | end 254 | add('') 255 | end 256 | add('\n" .. s .. "" 262 | end 263 | 264 | -- The following code will produce runtime warnings when you haven't defined 265 | -- all of the functions you need for the custom writer, so it's useful 266 | -- to include when you're working on a writer. 267 | local meta = {} 268 | meta.__index = 269 | function(_, key) 270 | io.stderr:write(string.format("WARNING: Undefined function '%s'\n",key)) 271 | return function() return "" end 272 | end 273 | setmetatable(_G, meta) 274 | 275 | 276 | 277 | function LineBreak() 278 | return "\n" 279 | end 280 | 281 | function Emph(s) 282 | return "_" .. s .. "_" 283 | end 284 | 285 | function Strong(s) 286 | return "*" .. s .. "*" 287 | end 288 | 289 | function Strikeout(s) 290 | return '~' .. s .. '~' 291 | end 292 | 293 | function Code(s, attr) 294 | return '```' .. s .. '```' 295 | end 296 | 297 | function Header(lev, s, attr) 298 | return '*' .. s .. '*' 299 | end 300 | 301 | function BulletList(items) 302 | local buffer = {} 303 | for _, item in pairs(items) do 304 | table.insert(buffer, "* " .. item) 305 | end 306 | return table.concat(buffer, "\n") .. "\n" 307 | end 308 | --------------------------------------------------------------------------------
" .. caption .. "
' .. h .. '
' .. c .. '