├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ └── publish.yml ├── .gitignore ├── README.md ├── README.qmd ├── _extensions └── now │ ├── _extension.yml │ └── now.lua ├── auto-dark-mode.css └── tests ├── test-modified.md └── test-modified.qmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^\.github$ 2 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | workflow_dispatch: 6 | 7 | name: Render docs 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | build-deploy: 20 | runs-on: ubuntu-latest 21 | 22 | environment: 23 | name: github-pages 24 | url: ${{ steps.deployment.outputs.page_url }} 25 | 26 | steps: 27 | - name: Check out repository 28 | uses: actions/checkout@v4 29 | 30 | - name: Set up Quarto 31 | uses: quarto-dev/quarto-actions/setup@v2 32 | 33 | - name: Render README 34 | shell: bash 35 | run: quarto render README.qmd --to html --output index.html 36 | 37 | # From GitHub's "Static HTML" workflow template 38 | - name: Setup Pages 39 | uses: actions/configure-pages@v4 40 | 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v3 43 | with: 44 | path: '.' 45 | 46 | - name: Deploy to GitHub Pages 47 | id: deployment 48 | uses: actions/deploy-pages@v4 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.pdf 3 | *_files/ 4 | /.luarc.json 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # now 2 | 3 | 4 | A Quarto extension for inserting the current or last modified date and 5 | time in the format of your choosing. 6 | 7 | ## Installing 8 | 9 | ``` bash 10 | quarto add gadenbuie/quarto-now 11 | ``` 12 | 13 | This will install the **now** extension under the `_extensions` 14 | subdirectory. If you’re using version control, you will want to check in 15 | this directory. 16 | 17 | ## Get Started 18 | 19 | The **now** extension provides two shortcodes: `now` and `modified`. 20 | Both take an optional additional argument specifying the desired format 21 | of the date and/or time. 22 | 23 | ### Last rendered time 24 | 25 | Use the `{{< now >}}` shortcode anywhere you want to include the current 26 | date and/or time in your document. 27 | 28 | ``` markdown 29 | Document last rendered: {{< now >}}. 30 | ``` 31 | 32 | > Document last rendered: 2024-03-29 09:22:32. 33 | 34 | > [!TIP] 35 | > 36 | > ### What does “last rendered” mean? 37 | > 38 | > “Last rendered” means the date and time when you called 39 | > `quarto render` or when the document was rendered by `quarto preview`. 40 | > 41 | > Because `now` is a shortcode, its value is re-calculated every time 42 | > the document renders, even if you’ve frozen the document with 43 | > `freeze: true`. Frozen documents don’t re-run computed code chunks, 44 | > but their shortcodes are still re-evaluated. 45 | > 46 | > So `{{< now >}}` is best for things like copyright notices in footers 47 | > – try `{{< now year >}}` for that one – or other places where you know 48 | > that `quarto render` is synonymous with “last updated”. 49 | 50 | ### Last modified time 51 | 52 | Alternatively, you can use the `{{< modified >}}` shortcode to include 53 | the last modified date and/or time of the document. This shortcode uses 54 | the `modified` metadata field to determine the last modified date and 55 | time of the document. On macOS and Linux, the modified timestamp can be 56 | automatically determined from the source file. 57 | 58 | ``` markdown 59 | --- 60 | modified: "2006-05-04 12:34:56" 61 | --- 62 | 63 | Document last modified: {{< modified >}}. 64 | ``` 65 | 66 | > Document last modified: 2024-03-29 09:22:31. 67 | 68 | > [!CAUTION] 69 | > 70 | > ### Automatic file-modified detection 71 | > 72 | > Automatic file-modified detection is only available on macOS and 73 | > Linux, since it relies the `stat` command to determine the last 74 | > modified time of the input document. 75 | > 76 | > If you’re on Windows, or if your system doesn’t support `stat`, you 77 | > can add `modified` to the document metadata to specify the last 78 | > modified date and time. Use `YYYY-MM-DD` format with optional time 79 | > `HH:MM:SS` in 24-hour format. 80 | > 81 | > ``` markdown 82 | > --- 83 | > modified: 2006-05-04 84 | > # -- or -- 85 | > modified: "2006-05-04 12:30" 86 | > --- 87 | > ``` 88 | 89 | ## Formatting 90 | 91 | Both `{{< now >}}` and `{{< modified >}}` shortcodes accept an optional 92 | argument specifying the desired format of the date and/or time. 93 | 94 | ### Aliases 95 | 96 | You can use one of the predefined format aliases in the table below. 97 | 98 | | Shortcode | Result | Format String | 99 | |:--------------------------|:-------------------------|:-------------:| 100 | | `{{< now >}}` | 2024-03-29 09:22:32 | `"%F %T"` | 101 | | `{{< now year >}}` | 2024 | `"%Y"` | 102 | | `{{< now month >}}` | March | `"%B"` | 103 | | `{{< now day >}}` | 29 | `"%d"` | 104 | | `{{< now weekday >}}` | Friday | `"%A"` | 105 | | `{{< now hour >}}` | 09 | `"%I"` | 106 | | `{{< now minute >}}` | 22 | `"%M"` | 107 | | `{{< now ampm >}}` | AM | `"%p"` | 108 | | `{{< now date >}}` | 03/29/24 | `"%x"` | 109 | | `{{< now time >}}` | 09:22:32 | `"%X"` | 110 | | `{{< now datetime >}}` | Fri Mar 29 09:22:32 2024 | `"%c"` | 111 | | `{{< now isodate >}}` | 2024-03-29 | `"%F"` | 112 | | `{{< now isotime >}}` | 09:22:32 | `"%T"` | 113 | | `{{< now isodatetime >}}` | 2024-03-29T09:22:32-0400 | `"%FT%T%z"` | 114 | | `{{< now timestamp >}}` | 2024-03-29 09:22:32 | `"%F %T"` | 115 | 116 | ### Format Strings 117 | 118 | Alternatively, you can specify the specific format using the format 119 | strings known to [the Lua `os.date()` 120 | function](https://www.lua.org/pil/22.1.html). 121 | 122 | | Value | Description | 123 | |:------|:---------------------------------------------| 124 | | `%a` | abbreviated weekday name (e.g., `Wed`) | 125 | | `%A` | full weekday name (e.g., `Wednesday`) | 126 | | `%b` | abbreviated month name (e.g., `Sep`) | 127 | | `%B` | full month name (e.g., `September`) | 128 | | `%c` | date and time (e.g., `09/16/98 23:48:10`) | 129 | | `%d` | day of the month (`16`) \[01-31\] | 130 | | `%H` | hour, using a 24-hour clock (`23`) \[00-23\] | 131 | | `%I` | hour, using a 12-hour clock (`11`) \[01-12\] | 132 | | `%M` | minute (`48`) \[00-59\] | 133 | | `%m` | month (`09`) \[01-12\] | 134 | | `%p` | either `"am"` or `"pm"` (`pm`) | 135 | | `%S` | second (`10`) \[00-61\] | 136 | | `%w` | weekday (`3`) \[0-6 = Sunday-Saturday\] | 137 | | `%x` | date (e.g., `09/16/98`) | 138 | | `%X` | time (e.g., `23:48:10`) | 139 | | `%Y` | full year (`1998`) | 140 | | `%y` | two-digit year (`98`) \[00-99\] | 141 | | `%%` | the character `%` | 142 | 143 | When using a custom format string, you can include any additional text 144 | you want. If your format string includes a space, be sure to wrap the 145 | format string in quotes. 146 | 147 | ``` markdown 148 | Modified {{< modified "on %A, %B %d of %Y" >}}. 149 | ``` 150 | 151 | > Modified on Friday, March 29 of 2024. 152 | -------------------------------------------------------------------------------- /README.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "now" 3 | subtitle: "A Quarto extension for inserting the current or last modified date and time" 4 | 5 | format: 6 | gfm: default 7 | html: 8 | css: auto-dark-mode.css 9 | toc: true 10 | toc-level: 2 11 | toc-location: right 12 | toc-title: "now" 13 | code-links: 14 | - text: gadenbuie/quarto-now 15 | href: "https://github.com/gadenbuie/quarto-now" 16 | icon: github 17 | - text: Question or Issue? 18 | href: "https://github.com/gadenbuie/quarto-now/issues/new" 19 | icon: question-circle-fill 20 | 21 | format-links: false 22 | 23 | # Comment out the line below to test automatic file-modified detection 24 | modified: "2006-05-04 12:34:56" 25 | --- 26 | 27 | ::: {.content-visible when-format="markdown"} 28 | A Quarto extension for inserting the current or last modified date and time in the format of your choosing. 29 | ::: 30 | 31 | ## Installing 32 | 33 | ```bash 34 | quarto add gadenbuie/quarto-now 35 | ``` 36 | 37 | This will install the **now** extension under the `_extensions` subdirectory. 38 | If you're using version control, you will want to check in this directory. 39 | 40 | ## Get Started 41 | 42 | The **now** extension provides two shortcodes: `now` and `modified`. 43 | Both take an optional additional argument specifying the desired format of the date and/or time. 44 | 45 | ### Last rendered time 46 | 47 | Use the `{{{< now >}}}` shortcode anywhere you want to include the current date and/or time in your document. 48 | 49 | ```{.markdown shortcodes="false"} 50 | Document last rendered: {{< now >}}. 51 | ``` 52 | 53 | > Document last rendered: {{< now >}}. 54 | 55 | ::: {.callout-tip collapse="true" title="What does \"last rendered\" mean?"} 56 | "Last rendered" means the date and time when you called `quarto render` or when the document was rendered by `quarto preview`. 57 | 58 | Because `now` is a shortcode, its value is re-calculated every time the document renders, even if you've frozen the document with `freeze: true`. 59 | Frozen documents don't re-run computed code chunks, but their shortcodes are still re-evaluated. 60 | 61 | So `{{{< now >}}}` is best for things like copyright notices in footers -- try `{{{< now year >}}}` for that one -- or other places where you know that `quarto render` is synonymous with "last updated". 62 | ::: 63 | 64 | ### Last modified time 65 | 66 | Alternatively, you can use the `{{{< modified >}}}` shortcode to include the last modified date and/or time of the document. 67 | This shortcode uses the `modified` metadata field to determine the last modified date and time of the document. 68 | On macOS and Linux, the modified timestamp can be automatically determined from the source file. 69 | 70 | ```{.markdown shortcodes="false"} 71 | --- 72 | modified: "2006-05-04 12:34:56" 73 | --- 74 | 75 | Document last modified: {{< modified >}}. 76 | ``` 77 | 78 | > Document last modified: {{< modified >}}. 79 | 80 | ::: {.callout-caution collapse="false" title="Automatic file-modified detection"} 81 | Automatic file-modified detection is only available on macOS and Linux, since it relies the `stat` command to determine the last modified time of the input document. 82 | 83 | If you're on Windows, or if your system doesn't support `stat`, you can add `modified` to the document metadata to specify the last modified date and time. 84 | Use `YYYY-MM-DD` format with optional time `HH:MM:SS` in 24-hour format. 85 | 86 | ```markdown 87 | --- 88 | modified: 2006-05-04 89 | # -- or -- 90 | modified: "2006-05-04 12:30" 91 | --- 92 | ``` 93 | ::: 94 | 95 | ## Formatting 96 | 97 | Both `{{{< now >}}}` and `{{{< modified >}}}` shortcodes accept an optional argument specifying the desired format of the date and/or time. 98 | 99 | ### Aliases 100 | 101 | You can use one of the predefined format aliases in the table below. 102 | 103 | | Shortcode | Result | Format String | 104 | |:----------------------------|:------------------------|:-------------:| 105 | | `{{{< now >}}}` | {{< now >}} | `"%F %T"` | 106 | | `{{{< now year >}}}` | {{< now year >}} | `"%Y"` | 107 | | `{{{< now month >}}}` | {{< now month >}} | `"%B"` | 108 | | `{{{< now day >}}}` | {{< now day >}} | `"%d"` | 109 | | `{{{< now weekday >}}}` | {{< now weekday >}} | `"%A"` | 110 | | `{{{< now hour >}}}` | {{< now hour >}} | `"%I"` | 111 | | `{{{< now minute >}}}` | {{< now minute >}} | `"%M"` | 112 | | `{{{< now ampm >}}}` | {{< now ampm >}} | `"%p"` | 113 | | `{{{< now date >}}}` | {{< now date >}} | `"%x"` | 114 | | `{{{< now time >}}}` | {{< now time >}} | `"%X"` | 115 | | `{{{< now datetime >}}}` | {{< now datetime >}} | `"%c"` | 116 | | `{{{< now isodate >}}}` | {{< now isodate >}} | `"%F"` | 117 | | `{{{< now isotime >}}}` | {{< now isotime >}} | `"%T"` | 118 | | `{{{< now isodatetime >}}}` | {{< now isodatetime >}} | `"%FT%T%z"` | 119 | | `{{{< now timestamp >}}}` | {{< now timestamp >}} | `"%F %T"` | 120 | 121 | ### Format Strings 122 | 123 | Alternatively, you can specify the specific format using the format strings known to [the Lua `os.date()` function](https://www.lua.org/pil/22.1.html). 124 | 125 | | Value | Description | 126 | |:------|:---------------------------------------------| 127 | | `%a` | abbreviated weekday name (e.g., `Wed`) | 128 | | `%A` | full weekday name (e.g., `Wednesday`) | 129 | | `%b` | abbreviated month name (e.g., `Sep`) | 130 | | `%B` | full month name (e.g., `September`) | 131 | | `%c` | date and time (e.g., `09/16/98 23:48:10`) | 132 | | `%d` | day of the month (`16`) \[01-31\] | 133 | | `%H` | hour, using a 24-hour clock (`23`) \[00-23\] | 134 | | `%I` | hour, using a 12-hour clock (`11`) \[01-12\] | 135 | | `%M` | minute (`48`) \[00-59\] | 136 | | `%m` | month (`09`) \[01-12\] | 137 | | `%p` | either `"am"` or `"pm"` (`pm`) | 138 | | `%S` | second (`10`) \[00-61\] | 139 | | `%w` | weekday (`3`) \[0-6 = Sunday-Saturday\] | 140 | | `%x` | date (e.g., `09/16/98`) | 141 | | `%X` | time (e.g., `23:48:10`) | 142 | | `%Y` | full year (`1998`) | 143 | | `%y` | two-digit year (`98`) \[00-99\] | 144 | | `%%` | the character `%` | 145 | 146 | When using a custom format string, you can include any additional text you want. 147 | If your format string includes a space, be sure to wrap the format string in quotes. 148 | 149 | ```{.markdown shortcodes="false"} 150 | Modified {{< modified "on %A, %B %d of %Y" >}}. 151 | ``` 152 | 153 | > Modified {{< modified "on %A, %B %d of %Y" >}}. 154 | -------------------------------------------------------------------------------- /_extensions/now/_extension.yml: -------------------------------------------------------------------------------- 1 | title: Now 2 | author: Garrick Aden-Buie 3 | version: 1.0.0 4 | quarto-required: ">=1.4.0" 5 | contributes: 6 | shortcodes: 7 | - now.lua 8 | 9 | -------------------------------------------------------------------------------- /_extensions/now/now.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | # MIT License 3 | # 4 | # Copyright (c) 2024 Garrick Aden-Buie 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in all 14 | # copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | ]] 24 | 25 | local formatMapping = { 26 | year = "%Y", 27 | month = "%B", 28 | day = "%d", 29 | weekday = "%A", 30 | hour = "%I", 31 | minute = "%M", 32 | ampm = "%p", 33 | date = "%x", 34 | time = "%X", 35 | datetime = "%c", 36 | isodate = "%F", 37 | isotime = "%T", 38 | isodatetime = "%FT%T%z", 39 | timestamp = "%F %T" 40 | } 41 | 42 | local function run_command(command) 43 | local handle, err = io.popen(command) 44 | if not handle then 45 | quarto.log.error("Error running command `" .. command .. "`: " .. err) 46 | return nil 47 | end 48 | local result = handle:read("*a") 49 | handle:close() 50 | return result 51 | end 52 | 53 | local function last_modified_bsd(file) 54 | local command = "stat -f %m " .. file -- Command to get modification time 55 | local result = run_command(command) 56 | return tonumber(result) 57 | end 58 | 59 | local function last_modified_linux(file) 60 | local command = "stat -c %y " .. file -- Command to get modification time 61 | 62 | local result = run_command(command) 63 | if result == nil then 64 | return nil 65 | end 66 | 67 | -- Extract modification time string 68 | local mod_time_str = string.match(result, "(%d+%-%d+%-%d+ %d+:%d+:%d+)") 69 | if mod_time_str == nil then 70 | quarto.log.error( 71 | "Error parsing the file modification time string, " .. 72 | "defaulting to render time." 73 | ) 74 | return nil 75 | end 76 | 77 | local mod_time_pattern = "(%d+)-(%d+)-(%d+) (%d+):(%d+):(%d+)" 78 | local year, month, day, hour, min, sec = mod_time_str:match(mod_time_pattern) 79 | if not (year and month and day and hour and min and sec) then 80 | quarto.log.error( 81 | "Error parsing the file modification time string, " .. 82 | "defaulting to render time." 83 | ) 84 | return nil 85 | end 86 | 87 | return os.time{year=year, month=month, day=day, hour=hour, min=min, sec=sec} 88 | end 89 | 90 | local last_modified_fns = { 91 | linux = last_modified_linux, 92 | bsd = last_modified_bsd, 93 | darwin = last_modified_bsd 94 | } 95 | 96 | local function parse_modified_date(meta) 97 | if meta.modified == nil then 98 | return nil 99 | end 100 | 101 | local dt = pandoc.utils.stringify(meta.modified) 102 | 103 | -- Parsing the date string 104 | local year, month, day = dt:match("(%d%d%d%d)-(%d%d)-(%d%d)") 105 | if not (year and month and day) then 106 | quarto.log.error("Invalid `modified` date format. Please use the format 'YYYY-MM-DD HH:MM:SS'") 107 | return nil 108 | end 109 | 110 | -- parse the time in pieces so we can handle missing parts 111 | local hour = dt:match("%d%d%d%d-%d%d-%d%d[ T]?(%d+)") 112 | local min = dt:match("%d%d%d%d-%d%d-%d%d[ T]?%d+:(%d+)") 113 | local sec = dt:match("%d%d%d%d-%d%d-%d%d[ T]?%d+:%d+:(%d+)") 114 | 115 | -- Convert to a time object 116 | return os.time{ 117 | year=year, 118 | month=month, 119 | day=day, 120 | hour=hour or 0, 121 | min=min or 0, 122 | sec=sec or 0 123 | } 124 | end 125 | 126 | local function source_modified_time() 127 | local file = quarto.doc.input_file 128 | local last_modified = last_modified_fns[pandoc.system.os](file) 129 | 130 | return last_modified 131 | end 132 | 133 | local function get_format(args) 134 | local format = formatMapping[args[1]] or args[1] or "%F %T" 135 | format = pandoc.utils.stringify(format) 136 | return format 137 | end 138 | 139 | ---@param format string The format string, see https://www.lua.org/pil/22.1.html 140 | ---@param mod_time integer? 141 | ---@return pandoc.Str 142 | local function format_time(format, mod_time) 143 | local now = os.date(format, mod_time) 144 | return pandoc.Str(tostring(now)) 145 | end 146 | 147 | return { 148 | ['now'] = function(args) 149 | local format = get_format(args) 150 | return format_time(format) 151 | end, 152 | ['modified'] = function(args, _, meta) 153 | local format = get_format(args) 154 | 155 | local modified = parse_modified_date(meta) 156 | if modified then 157 | return format_time(format, modified) 158 | end 159 | 160 | local os = pandoc.system.os 161 | if last_modified_fns[os] == nil then 162 | quarto.log.warning( 163 | '`modified` shortcode can\'t automatically discover ' .. 164 | 'the file modification time on "' .. os .. 165 | '", using rendered time instead. You can manually set the ' .. 166 | '`modified` date in the metadata to avoid this warning.' 167 | ) 168 | return format_time(format) 169 | end 170 | 171 | mod_time = source_modified_time() 172 | return format_time(format, mod_time) 173 | end 174 | } 175 | -------------------------------------------------------------------------------- /auto-dark-mode.css: -------------------------------------------------------------------------------- 1 | /* https://ar.al/2021/08/24/implementing-dark-mode-in-a-handful-of-lines-of-css-with-css-filters/ */ 2 | @media (prefers-color-scheme: dark) { 3 | /* Invert all elements on the body while attempting to not alter the hue substantially. */ 4 | body { 5 | filter: invert(100%) hue-rotate(180deg); 6 | } 7 | 8 | /* Workarounds and optical adjustments. */ 9 | 10 | /* 11 | Firefox workaround: Set the background colour for the html 12 | element separately because, unlike other browsers, Firefox 13 | doesn't apply the filter to the root element's background. 14 | */ 15 | html { 16 | background-color: #111; 17 | } 18 | 19 | /* Do not invert media (revert the invert). */ 20 | img, video, iframe, svg { 21 | filter: invert(100%) hue-rotate(180deg); 22 | } 23 | 24 | /* 25 | Videos running fullscreen are no longer affected by the 26 | filter on the body so we need to also unset the 27 | revert we applied earlier so we're left with no filter again. 28 | */ 29 | video:fullscreen { 30 | filter: none; 31 | } 32 | 33 | /* Improve contrast on icons. */ 34 | .icon { 35 | filter: invert(15%) hue-rotate(180deg); 36 | } 37 | 38 | /* Re-enable code block backgrounds. */ 39 | pre { 40 | filter: invert(6%); 41 | } 42 | 43 | /* Improve contrast on list item markers. */ 44 | li::marker { 45 | color: #666; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/test-modified.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | The next two lines should match in the rendered output. 4 | 5 | Document last modified: 2006-05-04 00:00:00. 6 | Document last modified: 2006-05-04 00:00:00. 7 | -------------------------------------------------------------------------------- /tests/test-modified.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | format: gfm 3 | modified: "2006-05-04" 4 | shortcodes: 5 | - ../_extensions/now/now.lua 6 | --- 7 | 8 | The next two lines should match in the rendered output. 9 | 10 | Document last modified: {{< modified >}}. \ 11 | Document last modified: 2006-05-04 00:00:00. 12 | 13 | --------------------------------------------------------------------------------