├── .editorconfig ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .tools └── docs.lua ├── LICENSE ├── Makefile ├── README.md ├── _extensions └── pretty-urls │ ├── _extension.yml │ └── pretty-urls.lua ├── pretty-urls.lua └── test ├── expected.native ├── input.md └── test.yaml /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [Makefile] 14 | indent_style = tab 15 | 16 | [*.lua] 17 | indent_style = space 18 | indent_size = 2 19 | # Code should stay below 80 characters per line. 20 | max_line_length = 80 21 | 22 | [*.md] 23 | # Text with 60 to 66 characters per line is said to be the easiest 24 | # to read. 25 | max_line_length = 66 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # Run on all pull requests that change code. 5 | pull_request: 6 | paths-ignore: 7 | - 'README.md' 8 | - LICENSE 9 | - .editorconfig 10 | # Run every time a code change is pushed. 11 | push: 12 | paths-ignore: 13 | - 'README.md' 14 | - LICENSE 15 | - .editorconfig 16 | # Test if things still work each Tuesday morning at 5:39 UTC. 17 | # This way we will catch incompatible pandoc changes in a timely 18 | # manner. 19 | schedule: 20 | # At 5:39am each Tuesday 21 | - cron: '39 5 * * 2' 22 | 23 | jobs: 24 | test: 25 | runs-on: ubuntu-latest 26 | strategy: 27 | fail-fast: true 28 | matrix: 29 | pandoc: 30 | - edge 31 | - latest 32 | # This should be the oldest version that's supported 33 | # - 2.19.2 34 | 35 | container: 36 | image: pandoc/core:${{ matrix.pandoc }} 37 | 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v2 41 | 42 | - name: Install dependencies 43 | run: apk add make 44 | 45 | - name: Test 46 | run: make test 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_site/ 2 | -------------------------------------------------------------------------------- /.tools/docs.lua: -------------------------------------------------------------------------------- 1 | local path = require 'pandoc.path' 2 | local utils = require 'pandoc.utils' 3 | local stringify = utils.stringify 4 | 5 | local function read_file (filename) 6 | local fh = io.open(filename) 7 | local content = fh:read('*a') 8 | fh:close() 9 | return content 10 | end 11 | 12 | local formats_by_extension = { 13 | md = 'markdown', 14 | latex = 'latex', 15 | native = 'haskell', 16 | tex = 'latex', 17 | } 18 | 19 | local function sample_blocks (sample_file) 20 | local sample_content = read_file(sample_file) 21 | local extension = select(2, path.split_extension(sample_file)):sub(2) 22 | local format = formats_by_extension[extension] or extension 23 | local filename = path.filename(sample_file) 24 | 25 | local sample_attr = pandoc.Attr('', {format, 'sample'}) 26 | return { 27 | pandoc.Header(3, pandoc.Str(filename), {filename}), 28 | pandoc.CodeBlock(sample_content, sample_attr) 29 | } 30 | end 31 | 32 | local function result_blocks (result_file) 33 | local result_content = read_file(result_file) 34 | local extension = select(2, path.split_extension(result_file)):sub(2) 35 | local format = formats_by_extension[extension] or extension 36 | local filename = path.filename(result_file) 37 | 38 | local result_attr = pandoc.Attr('', {format, 'sample'}) 39 | return { 40 | pandoc.Header(3, pandoc.Str(filename), {filename}), 41 | pandoc.CodeBlock(result_content, result_attr) 42 | } 43 | end 44 | 45 | local function code_blocks (code_file) 46 | local code_content = read_file(code_file) 47 | local code_attr = pandoc.Attr(code_file, {'lua'}) 48 | return { 49 | pandoc.CodeBlock(code_content, code_attr) 50 | } 51 | end 52 | 53 | function Pandoc (doc) 54 | local meta = doc.meta 55 | local blocks = doc.blocks 56 | 57 | -- Set document title from README title. There should usually be just 58 | -- a single level 1 heading. 59 | blocks = blocks:walk{ 60 | Header = function (h) 61 | if h.level == 1 then 62 | meta.title = h.content 63 | return {} 64 | end 65 | end 66 | } 67 | 68 | -- Add the sample file as an example. 69 | blocks:extend{pandoc.Header(2, 'Example', pandoc.Attr('Example'))} 70 | blocks:extend(sample_blocks(stringify(meta['sample-file']))) 71 | blocks:extend(result_blocks(stringify(meta['result-file']))) 72 | 73 | -- Add the filter code. 74 | local code_file = stringify(meta['code-file']) 75 | blocks:extend{pandoc.Header(2, 'Code', pandoc.Attr('Code'))} 76 | blocks:extend{pandoc.Para{pandoc.Link(pandoc.Str(code_file), code_file)}} 77 | blocks:extend(code_blocks(code_file)) 78 | 79 | return pandoc.Pandoc(blocks, meta) 80 | end 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2021–2022 Albert Krewinkel and contributors. 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Name of the filter file, *with* `.lua` file extension. 2 | FILTER_FILE := $(wildcard *.lua) 3 | # Name of the filter, *without* `.lua` file extension 4 | FILTER_NAME = $(patsubst %.lua,%,$(FILTER_FILE)) 5 | 6 | # Allow to use a different pandoc binary, e.g. when testing. 7 | PANDOC ?= pandoc 8 | # Allow to adjust the diff command if necessary 9 | DIFF = diff 10 | 11 | # Directory containing the Quarto extension 12 | QUARTO_EXT_DIR = _extensions/$(FILTER_NAME) 13 | # The extension's name. Used in the Quarto extension metadata 14 | EXT_NAME = $(FILTER_NAME) 15 | # Current version, i.e., the latest tag. Used to version the quarto 16 | # extension. 17 | VERSION = $(shell git tag --sort=-version:refname --merged | head -n1 | \ 18 | sed -e 's/^v//' | tr -d "\n") 19 | ifeq "$(VERSION)" "" 20 | VERSION = 0.0.0 21 | endif 22 | 23 | # GitHub repository; used to setup the filter. 24 | REPO_PATH = $(shell git remote get-url origin | sed -e 's%.*github\.com[/:]%%') 25 | REPO_NAME = $(shell git remote get-url origin | sed -e 's%.*/%%') 26 | USER_NAME = $(shell git config user.name) 27 | 28 | # Test that running the filter on the sample input document yields 29 | # the expected output. 30 | # 31 | # The automatic variable `$<` refers to the first dependency 32 | # (i.e., the filter file). 33 | test: $(FILTER_FILE) test/input.md test/test.yaml 34 | $(PANDOC) --defaults test/test.yaml | \ 35 | $(DIFF) test/expected.native - 36 | 37 | # Ensure that the `test` target is run each time it's called. 38 | .PHONY: test 39 | 40 | # Re-generate the expected output. This file **must not** be a 41 | # dependency of the `test` target, as that would cause it to be 42 | # regenerated on each run, making the test pointless. 43 | test/expected.native: $(FILTER_FILE) test/input.md test/test.yaml 44 | $(PANDOC) --defaults test/test.yaml --output=$@ 45 | 46 | # 47 | # Quarto extension 48 | # 49 | 50 | # Creates or updates the quarto extension 51 | .PHONY: quarto-extension 52 | quarto-extension: $(QUARTO_EXT_DIR)/_extension.yml \ 53 | $(QUARTO_EXT_DIR)/$(FILTER_FILE) 54 | 55 | $(QUARTO_EXT_DIR): 56 | mkdir -p $@ 57 | 58 | ## This may change, so re-create the file every time 59 | .PHONY: $(QUARTO_EXT_DIR)/_extension.yml 60 | $(QUARTO_EXT_DIR)/_extension.yml: _extensions/$(FILTER_NAME) 61 | @printf 'Creating %s\n' $@ 62 | @printf 'name: %s\n' "$(EXT_NAME)" > $@ 63 | @printf 'author: %s\n' "$(USER_NAME)" >> $@ 64 | @printf 'version: %s\n' "$(VERSION)" >> $@ 65 | @printf 'contributes:\n filters:\n - %s\n' $(FILTER_FILE) >> $@ 66 | 67 | ## The filter file must be below the quarto _extensions folder: a 68 | ## symlink in the extension would not work due to the way in which 69 | ## quarto installs extensions. 70 | $(QUARTO_EXT_DIR)/$(FILTER_FILE): $(FILTER_FILE) $(QUARTO_EXT_DIR) 71 | if [ ! -L $(FILTER_FILE) ]; then \ 72 | mv $(FILTER_FILE) $(QUARTO_EXT_DIR)/$(FILTER_FILE) && \ 73 | ln -s $(QUARTO_EXT_DIR)/$(FILTER_FILE) $(FILTER_FILE); \ 74 | fi 75 | 76 | # 77 | # Release 78 | # 79 | .PHONY: release 80 | release: quarto-extension 81 | git commit -am "Release $(FILTER_NAME) $(VERSION)" 82 | git tag v$(VERSION) -m "$(FILTER_NAME) $(VERSION)" 83 | 84 | # 85 | # Update filter name 86 | # 87 | .PHONY: update-name 88 | update-name: 89 | sed -i'' -e 's/greetings/$(FILTER_NAME)/g' README.md 90 | sed -i'' -e 's/greetings/$(FILTER_NAME)/g' test/test.yaml 91 | 92 | setup: 93 | git mv greetings.lua $(REPO_NAME).lua 94 | @# Crude method to updates the examples and links; removes the 95 | @# template instructions from the README. 96 | sed -i'' \ 97 | -e 's/greetings/$(REPO_NAME)/g' \ 98 | -e 's#tarleb/lua-filter-template#$(REPO_PATH)#g' \ 99 | -e '/^\* \*/,/^\* \*/d' \ 100 | README.md 101 | sed -i'' -e 's/greetings/$(REPO_NAME)/g' test/test.yaml 102 | sed -i'' -e 's/Albert Krewinkel/$(USER_NAME)/' LICENSE 103 | 104 | # 105 | # Clean 106 | # 107 | .PHONY: clean 108 | clean: 109 | rm -f _site/output.md _site/index.html _site/style.css 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pretty-urls 2 | ================================================================== 3 | 4 | [![GitHub build status][CI badge]][CI workflow] 5 | 6 | This filter "prettifies" bare URLs by removing the protocol 7 | prefix, i.e., it drops the `https://` from the link text while 8 | leaving the actual link unchanged. 9 | 10 | Example: the URL `` is rewritten as if it was 11 | defined as `[pandoc.org](https://pandoc.org)`. 12 | 13 | - **Before**: 14 | - **After**: [pandoc.org](https://pandoc.org) 15 | 16 | URLs for which the description is different from the target are 17 | left unchanged. 18 | 19 | [CI badge]: https://img.shields.io/github/actions/workflow/status/pandoc-ext/pretty-urls/ci.yaml?branch=main&logo=github 20 | [CI workflow]: https://github.com/pandoc-ext/pretty-urls/actions/workflows/ci.yaml 21 | 22 | 23 | Usage 24 | ------------------------------------------------------------------ 25 | 26 | The filter modifies the internal document representation; it can 27 | be used with many publishing systems that are based on pandoc. 28 | 29 | ### Plain pandoc 30 | 31 | Pass the filter to pandoc via the `--lua-filter` (or `-L`) command 32 | line option. 33 | 34 | pandoc --lua-filter pretty-urls.lua ... 35 | 36 | ### Quarto 37 | 38 | Users of Quarto can install this filter as an extension with 39 | 40 | quarto install extension tarleb/pretty-urls 41 | 42 | and use it by adding `pretty-urls` to the `filters` entry 43 | in their YAML header. 44 | 45 | ``` yaml 46 | --- 47 | filters: 48 | - pretty-urls 49 | --- 50 | ``` 51 | 52 | ### R Markdown 53 | 54 | Use `pandoc_args` to invoke the filter. See the [R Markdown 55 | Cookbook](https://bookdown.org/yihui/rmarkdown-cookbook/lua-filters.html) 56 | for details. 57 | 58 | ``` yaml 59 | --- 60 | output: 61 | word_document: 62 | pandoc_args: ['--lua-filter=pretty-urls.lua'] 63 | --- 64 | ``` 65 | 66 | License 67 | ------------------------------------------------------------------ 68 | 69 | This pandoc Lua filter is published under the MIT license, see 70 | file `LICENSE` for details. 71 | -------------------------------------------------------------------------------- /_extensions/pretty-urls/_extension.yml: -------------------------------------------------------------------------------- 1 | title: pretty-urls 2 | author: Albert Krewinkel 3 | version: 1.0.0 4 | contributes: 5 | filters: 6 | - pretty-urls.lua 7 | -------------------------------------------------------------------------------- /_extensions/pretty-urls/pretty-urls.lua: -------------------------------------------------------------------------------- 1 | --- pretty-urls.lua – let URLs look a little nicer 2 | --- 3 | --- Copyright: © 2022 Albert Krewinkel 4 | --- License: MIT – see LICENSE for details 5 | -- Ensure a recent enough pandoc version. 6 | PANDOC_VERSION:must_be_at_least '2.7' 7 | 8 | local stringify = pandoc.utils.stringify 9 | 10 | local function prettify_url (link) 11 | -- Do not change the link if the description does not match the 12 | -- target and it is not an autolink (marked by the 'uri' class). 13 | if stringify(link.content) ~= link.target and 14 | not link.classes:includes 'uri' then 15 | return nil 16 | end 17 | 18 | -- Remove http and https protocol prefix 19 | local is_unsafe_protocol = link.target:match '^http%:%/%/' ~= nil 20 | link_text = link.target 21 | :gsub('^https?%:%/%/', '') 22 | :gsub('^(d?x?%.?)doi%.org%/', 'doi:') --prettify DOIs 23 | 24 | if is_unsafe_protocol then 25 | link_text = link_text .. ' 🔓' 26 | end 27 | link.content = {pandoc.Str(link_text)} 28 | 29 | -- fix DOI links 30 | link.target = link.target:gsub('^doi%:', 'https://doi.org/') 31 | 32 | return link 33 | end 34 | 35 | return {{Link = prettify_url}} 36 | -------------------------------------------------------------------------------- /pretty-urls.lua: -------------------------------------------------------------------------------- 1 | _extensions/pretty-urls/pretty-urls.lua -------------------------------------------------------------------------------- /test/expected.native: -------------------------------------------------------------------------------- 1 | [ Header 2 | 1 3 | ( "tests-for-pretty-urls" , [] , [] ) 4 | [ Str "Tests" 5 | , Space 6 | , Str "for" 7 | , Space 8 | , Code ( "" , [] , [] ) "pretty-urls" 9 | ] 10 | , Header 2 ( "autourl" , [] , [] ) [ Str "AutoURL" ] 11 | , Para 12 | [ Link 13 | ( "" , [ "uri" ] , [] ) 14 | [ Str "pandoc.org" ] 15 | ( "https://pandoc.org" , "" ) 16 | ] 17 | , Header 18 | 2 19 | ( "content-differs-from-target" , [] , [] ) 20 | [ Str "Content" 21 | , Space 22 | , Str "differs" 23 | , Space 24 | , Str "from" 25 | , Space 26 | , Str "target" 27 | ] 28 | , Para 29 | [ Link 30 | ( "" , [] , [] ) 31 | [ Str "https://" 32 | , Space 33 | , Str "protocol" 34 | , Space 35 | , Str "prefix" 36 | ] 37 | ( "https://en.wikipedia.org/wiki/HTTPS" , "" ) 38 | ] 39 | , Header 40 | 2 41 | ( "link-with-special-chars" , [] , [] ) 42 | [ Str "Link" 43 | , Space 44 | , Str "with" 45 | , Space 46 | , Str "special" 47 | , Space 48 | , Str "chars" 49 | ] 50 | , Para 51 | [ Link 52 | ( "" , [ "uri" ] , [] ) 53 | [ Str "de.wikipedia.org/wiki/Lua_(Begriffskl%C3%A4rung)" ] 54 | ( "https://de.wikipedia.org/wiki/Lua_(Begriffskl%C3%A4rung)" 55 | , "" 56 | ) 57 | ] 58 | , Header 59 | 2 60 | ( "http-link" , [] , [] ) 61 | [ Str "HTTP" , Space , Str "link" ] 62 | , Para 63 | [ Link 64 | ( "" , [ "uri" ] , [] ) 65 | [ Str "archive.org\8201\128275" ] 66 | ( "http://archive.org" , "" ) 67 | ] 68 | , Header 69 | 2 70 | ( "doi-link" , [] , [] ) 71 | [ Str "DOI" , Space , Str "link" ] 72 | , Para 73 | [ Link 74 | ( "" , [ "uri" ] , [] ) 75 | [ Str "doi:10.7717/peerj-cs.112" ] 76 | ( "https://doi.org/10.7717/peerj-cs.112" , "" ) 77 | ] 78 | , Header 79 | 2 80 | ( "doi-protocol" , [] , [] ) 81 | [ Code ( "" , [] , [] ) "doi" , Space , Str "protocol" ] 82 | , Para 83 | [ Link 84 | ( "" , [ "uri" ] , [] ) 85 | [ Str "doi:10.7717/peerj-cs.112" ] 86 | ( "https://doi.org/10.7717/peerj-cs.112" , "" ) 87 | ] 88 | ] 89 | -------------------------------------------------------------------------------- /test/input.md: -------------------------------------------------------------------------------- 1 | # Tests for `pretty-urls` 2 | 3 | ## AutoURL 4 | 5 | 6 | 7 | ## Content differs from target 8 | 9 | [https:// protocol prefix](https://en.wikipedia.org/wiki/HTTPS) 10 | 11 | ## Link with special chars 12 | 13 | 14 | 15 | ## HTTP link 16 | 17 | 18 | 19 | ## DOI link 20 | 21 | 22 | 23 | ## `doi` protocol 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/test.yaml: -------------------------------------------------------------------------------- 1 | input-files: ["test/input.md"] 2 | to: native 3 | standalone: false 4 | filters: 5 | - {type: lua, path: pretty-urls.lua} 6 | --------------------------------------------------------------------------------