Compare commits
10 Commits
279312da51
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
4ce9cc1fb0
|
|||
|
0f0ba243d5
|
|||
|
03031b5ca8
|
|||
|
07a101488b
|
|||
|
69d6a42f5c
|
|||
|
d85f72fc92
|
|||
|
cabb8291cb
|
|||
|
135712e042
|
|||
|
acb13112c4
|
|||
|
1ce18a4cd7
|
85
.github/workflows/build-release.yaml
vendored
Normal file
85
.github/workflows/build-release.yaml
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
name: Build PDF & Release
|
||||||
|
run-name: Build PDF and Release ${{ github.ref_name }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- '**.md'
|
||||||
|
- '.github/workflows/**'
|
||||||
|
- 'md-pdf.ron'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Setup Rust cache
|
||||||
|
uses: swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Install Typst
|
||||||
|
run: |
|
||||||
|
mkdir -p /usr/local/bin
|
||||||
|
curl -L -o typst.tar.xz https://github.com/typst/typst/releases/latest/download/typst-x86_64-unknown-linux-musl.tar.xz
|
||||||
|
tar -xJf typst.tar.xz --strip-components=1 -C /usr/local/bin/ typst-x86_64-unknown-linux-musl/typst
|
||||||
|
typst --version
|
||||||
|
|
||||||
|
- name: Install md-pdf (Rust)
|
||||||
|
run: |
|
||||||
|
cargo install md-pdf
|
||||||
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.23'
|
||||||
|
|
||||||
|
- name: Install svu (Go)
|
||||||
|
run: go install github.com/caarlos0/svu@latest
|
||||||
|
|
||||||
|
- name: Build PDF
|
||||||
|
run: |
|
||||||
|
md-pdf README.md
|
||||||
|
mv README.pdf PIS.pdf
|
||||||
|
|
||||||
|
# Calculate version based on commit messages (fix, feat, breaking)
|
||||||
|
- name: Calculate Next Version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
NEW_TAG=$(svu next)
|
||||||
|
echo "tag=$NEW_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "Next version : $NEW_TAG"
|
||||||
|
|
||||||
|
- name: Push Tag
|
||||||
|
run: |
|
||||||
|
git config user.name "Gitea Actions"
|
||||||
|
git config user.email "actions@gitea.local"
|
||||||
|
git tag ${{ steps.version.outputs.tag }}
|
||||||
|
git push origin ${{ steps.version.outputs.tag }}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Create release and upload PDF
|
||||||
|
# Note: softprops works very well on recent Gitea
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: ${{ steps.version.outputs.tag != '' }} # Safety check
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.version.outputs.tag }}
|
||||||
|
name: Release ${{ steps.version.outputs.tag }}
|
||||||
|
files: PIS.pdf
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
56
README.md
56
README.md
@@ -1,3 +1,13 @@
|
|||||||
|
---
|
||||||
|
title: "PIS"
|
||||||
|
subtitle: "Policy for Internal Security"
|
||||||
|
author: "Rémi Heredero "
|
||||||
|
language: "en"
|
||||||
|
tags: ["gpg", "ssh", "x509", "YubiKey", "security"]
|
||||||
|
toc: false
|
||||||
|
template: "simple"
|
||||||
|
---
|
||||||
|
|
||||||
# Policy for Internal Security
|
# Policy for Internal Security
|
||||||
This repo describes my P.I.S. (**P**olicy for **I**nternal **S**ecurity).
|
This repo describes my P.I.S. (**P**olicy for **I**nternal **S**ecurity).
|
||||||
You'll find my personal guidelines for SSH / GPG on YubiKey and how to configure and create a key / certificate.
|
You'll find my personal guidelines for SSH / GPG on YubiKey and how to configure and create a key / certificate.
|
||||||
@@ -195,11 +205,49 @@ This creates the file `id_ed25519_sk-keyring-cert.pub` that is the certificate t
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# x509
|
# LUKS
|
||||||
|
|
||||||
|
It's possible to add a Yubikey as a second option to unlock a LUKS partition.
|
||||||
|
|
||||||
|
The first step is to find the encrypted partition.
|
||||||
|
```bash
|
||||||
|
lsblk
|
||||||
|
```
|
||||||
|
`nvme1n1p3` is the encrypted partition in my case.
|
||||||
|
|
||||||
|
## Enroll
|
||||||
|
Add a new way to unlock the partition with the YubiKey. This add a FIDO device, not replace the password way. You can still unlock the partition with the password if you forget the YubiKey.
|
||||||
|
|
||||||
|
This step have to be done for each Yubikey you want to use to unlock the partition.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemd-cryptenroll --fido2-device=auto /dev/nvme1n1p3
|
||||||
|
```
|
||||||
|
Actual passphrase is requested, then Yubikey Fido2 PIN, then you have to touch it 2 time to confirme presence.
|
||||||
|
|
||||||
|
## Config `/etc/crypttab`
|
||||||
|
This step have to be only once.
|
||||||
|
|
||||||
|
Backup and edit crypttab
|
||||||
|
```bash
|
||||||
|
sudo cp /etc/crypttab /etc/crypttab.bak
|
||||||
|
sudo nano /etc/crypttab
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `,fido2-device=auto` (without any space) at the end of the line that describe the encrypted partition. It should look like that at the end:
|
||||||
|
|
||||||
|
```
|
||||||
|
luks-1234... UUID=1234... none discard,fido2-device=auto
|
||||||
|
```
|
||||||
|
|
||||||
|
## Re-Generate initramfs
|
||||||
|
This step have to be only once.
|
||||||
|
|
||||||
|
After enrolling the YubiKey, you need to re-generate the initramfs to be able to unlock the partition at boot time.
|
||||||
|
```bash
|
||||||
|
sudo dracut -f
|
||||||
|
```
|
||||||
|
|
||||||
## Master YubiKey
|
|
||||||
I create a certificate in PIV slot 9a with Yubico authenticator. This CA would be used as a Root CA for my server.
|
|
||||||
TODO fix with XCA
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
7
md-pdf.ron
Normal file
7
md-pdf.ron
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
(
|
||||||
|
templates_dir: Some("./templates"),
|
||||||
|
default_template: Some("simple"),
|
||||||
|
default_language: Some("en"),
|
||||||
|
default_toc: Some(true),
|
||||||
|
default_author: Some("Rémi Heredero"),
|
||||||
|
)
|
||||||
129
templates/none.typ
Normal file
129
templates/none.typ
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
|
||||||
|
#import "@preview/cmarker:0.1.7"
|
||||||
|
#import "@preview/mitex:0.2.6": mitex
|
||||||
|
|
||||||
|
// Get system inputs
|
||||||
|
#let filepath = sys.inputs.at("filepath", default: "input.md")
|
||||||
|
#let language = sys.inputs.at("language", default: "en")
|
||||||
|
#let show-toc = sys.inputs.at("toc", default: "false") == "true"
|
||||||
|
|
||||||
|
// Front matter inputs
|
||||||
|
#let has-frontmatter = sys.inputs.at("has_frontmatter", default: "false") == "true"
|
||||||
|
#let fm-title = sys.inputs.at("fm_title", default: none)
|
||||||
|
#let fm-subtitle = sys.inputs.at("fm_subtitle", default: none)
|
||||||
|
#let fm-author = sys.inputs.at("fm_author", default: none)
|
||||||
|
#let fm-date = sys.inputs.at("fm_date", default: none)
|
||||||
|
#let fm-tags = sys.inputs.at("fm_tags", default: none)
|
||||||
|
#let fm_version = sys.inputs.at("fm_version", default: none)
|
||||||
|
|
||||||
|
// Parse tags from comma-separated string
|
||||||
|
#let tags-list = if fm-tags != none { fm-tags.split(",") } else { () }
|
||||||
|
|
||||||
|
// Extract filename from filepath (remove path and .md extension)
|
||||||
|
#let filename = {
|
||||||
|
let path-parts = filepath.split("/")
|
||||||
|
let file = path-parts.last()
|
||||||
|
if file.ends-with(".md") {
|
||||||
|
file.slice(0, file.len() - 3)
|
||||||
|
} else if file.ends-with(".temp.md") {
|
||||||
|
file.slice(0, file.len() - 8)
|
||||||
|
} else {
|
||||||
|
file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use front matter data or defaults
|
||||||
|
#let document-author = if fm-author != none { fm-author } else { default_author }
|
||||||
|
#let document-title = if fm-title != none { fm-title } else { filename }
|
||||||
|
#let document-subtitle = if fm-subtitle != none { fm-subtitle } else { filename }
|
||||||
|
|
||||||
|
// Parse date
|
||||||
|
#let document-date = if fm-date != none {
|
||||||
|
// Try to parse the date string
|
||||||
|
let date-str = fm-date
|
||||||
|
if date-str.len() == 10 and date-str.contains("-") {
|
||||||
|
// Format: YYYY-MM-DD
|
||||||
|
let parts = date-str.split("-")
|
||||||
|
if parts.len() == 3 {
|
||||||
|
datetime(year: int(parts.at(0)), month: int(parts.at(1)), day: int(parts.at(2)))
|
||||||
|
} else {
|
||||||
|
datetime.today()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
datetime.today()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
datetime.today()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set document properties
|
||||||
|
#set document(
|
||||||
|
author: if document-author != none { document-author } else { "" },
|
||||||
|
title: document-title,
|
||||||
|
keywords: if fm-tags != none { (if document-author != none { document-author } else { "" }, document-title, "md-pdf", ..tags-list) } else { (if document-author != none { document-author } else { "" }, document-title, "md-pdf") },
|
||||||
|
date: document-date
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set document language
|
||||||
|
#set text(lang: language)
|
||||||
|
|
||||||
|
// Function to create tag labels
|
||||||
|
#let badge(content) = {
|
||||||
|
let color = rgb("888888")
|
||||||
|
let textcolor = rgb("222222")
|
||||||
|
box(
|
||||||
|
inset: (x: 3pt, y: 2pt),
|
||||||
|
radius: 4pt,
|
||||||
|
fill: color.lighten(70%),
|
||||||
|
stroke: (paint: color, thickness: 0.5pt),
|
||||||
|
)[
|
||||||
|
#text(weight: "bold", size: 6pt, fill:textcolor)[#content]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show basic document metadata if front matter exists
|
||||||
|
#if has-frontmatter [
|
||||||
|
#if fm-title != none [
|
||||||
|
#align(center)[
|
||||||
|
#text(size: 18pt, weight: "bold")[#fm-title]
|
||||||
|
]
|
||||||
|
#v(0.3em)
|
||||||
|
]
|
||||||
|
#if fm-subtitle != none [
|
||||||
|
#align(center)[
|
||||||
|
#text(size: 14pt, style: "italic")[#fm-subtitle]
|
||||||
|
]
|
||||||
|
#v(0.3em)
|
||||||
|
]
|
||||||
|
#let metadata = ()
|
||||||
|
#if document-author != none { metadata.push(document-author) }
|
||||||
|
#if document-date != none { metadata.push(document-date.display()) }
|
||||||
|
#if fm_version != none { metadata.push(fm_version) }
|
||||||
|
#align(center)[
|
||||||
|
#for (i, data) in metadata.enumerate() [
|
||||||
|
#data
|
||||||
|
#if i < metadata.len() - 1 [ \- ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
#if fm-tags != none and tags-list.len() > 0 [
|
||||||
|
#align(center)[
|
||||||
|
#for (i, tag) in tags-list.enumerate() [
|
||||||
|
#badge(tag.trim())
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
#line(length: 100%, stroke: 0.5pt)
|
||||||
|
]
|
||||||
|
|
||||||
|
// Show table of contents if requested
|
||||||
|
#if show-toc [
|
||||||
|
#outline()
|
||||||
|
#pagebreak()
|
||||||
|
]
|
||||||
|
|
||||||
|
#cmarker.render(
|
||||||
|
read(filepath),
|
||||||
|
scope: (image: (path, alt: none) => image(path, alt: alt)),
|
||||||
|
math: mitex
|
||||||
|
)
|
||||||
221
templates/simple.typ
Normal file
221
templates/simple.typ
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
#import "@preview/cmarker:0.1.8"
|
||||||
|
#import "@preview/mitex:0.2.6": mitex
|
||||||
|
#import "@preview/hei-synd-thesis:0.2.3": *
|
||||||
|
|
||||||
|
// Get system inputs
|
||||||
|
#let filepath = sys.inputs.at("filepath", default: "input.md")
|
||||||
|
#let language = sys.inputs.at("language", default: "en")
|
||||||
|
#let show-toc = sys.inputs.at("toc", default: "false") == "true"
|
||||||
|
|
||||||
|
// Front matter inputs
|
||||||
|
#let has-frontmatter = sys.inputs.at("has_frontmatter", default: "false") == "true"
|
||||||
|
#let fm-title = sys.inputs.at("fm_title", default: none)
|
||||||
|
#let fm-subtitle = sys.inputs.at("fm_subtitle", default: none)
|
||||||
|
#let fm-author = sys.inputs.at("fm_author", default: none)
|
||||||
|
#let fm-date = sys.inputs.at("fm_date", default: none)
|
||||||
|
#let fm-tags = sys.inputs.at("fm_tags", default: none)
|
||||||
|
#let fm_version = sys.inputs.at("fm_version", default: none)
|
||||||
|
|
||||||
|
// Parse tags from comma-separated string
|
||||||
|
#let tags-list = if fm-tags != none { fm-tags.split(",") } else { () }
|
||||||
|
|
||||||
|
// Extract filename from filepath (remove path and .md extension)
|
||||||
|
#let filename = {
|
||||||
|
let path-parts = filepath.split("/")
|
||||||
|
let file = path-parts.last()
|
||||||
|
if file.ends-with(".md") {
|
||||||
|
file.slice(0, file.len() - 3)
|
||||||
|
} else if file.ends-with(".temp.md") {
|
||||||
|
file.slice(0, file.len() - 8)
|
||||||
|
} else {
|
||||||
|
file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use front matter data or defaults
|
||||||
|
#let document-author = if fm-author != none { fm-author } else { none }
|
||||||
|
#let document-title = if fm-title != none { fm-title } else { filename }
|
||||||
|
#let document-subtitle = if fm-subtitle != none { fm-subtitle } else { none }
|
||||||
|
|
||||||
|
// Parse date
|
||||||
|
#let document-date = if fm-date != none {
|
||||||
|
// Try to parse the date string
|
||||||
|
let date-str = fm-date
|
||||||
|
if date-str.len() == 10 and date-str.contains("-") {
|
||||||
|
// Format: YYYY-MM-DD
|
||||||
|
let parts = date-str.split("-")
|
||||||
|
if parts.len() == 3 {
|
||||||
|
datetime(year: int(parts.at(0)), month: int(parts.at(1)), day: int(parts.at(2)))
|
||||||
|
} else {
|
||||||
|
datetime.today()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
datetime.today()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
datetime.today()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set document properties
|
||||||
|
#set document(
|
||||||
|
author: if document-author != none { document-author } else { "" },
|
||||||
|
title: document-title,
|
||||||
|
keywords: if fm-tags != none { (if document-author != none { document-author } else { "" }, document-title, "md-pdf", ..tags-list) } else { (document-author, document-title, "md-pdf") },
|
||||||
|
date: document-date
|
||||||
|
)
|
||||||
|
|
||||||
|
// basic properties
|
||||||
|
#set page(margin: (top:3cm, bottom:3cm, left:3cm, right:2.5cm))
|
||||||
|
|
||||||
|
// header and footer
|
||||||
|
#set page(
|
||||||
|
header: context(if here().page() >=2 [
|
||||||
|
#set text(small)
|
||||||
|
#smallcaps[#document-title] #if document-subtitle != none {[\/ #document-subtitle ]}
|
||||||
|
//#line(start: (-0.5em, 0cm), length: 85%, stroke: 0.5pt)
|
||||||
|
#line(start: (-0.5em, 0cm), length: 101%, stroke: 0.5pt)
|
||||||
|
]),
|
||||||
|
footer: context( if here().page() >=2 [
|
||||||
|
#set text(small)
|
||||||
|
#line(start: (85%, 0cm), length: 15%, stroke: 0.5pt)
|
||||||
|
|
||||||
|
#document-author / #document-date.display() #h(1fr) #context counter(page).display("1 / 1", both: true)
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
// font & language
|
||||||
|
#set text(
|
||||||
|
font: (
|
||||||
|
"Libertinus Serif",
|
||||||
|
"Fira Sans",
|
||||||
|
),
|
||||||
|
fallback: true,
|
||||||
|
lang: language
|
||||||
|
)
|
||||||
|
|
||||||
|
// heading
|
||||||
|
#show heading: set block(above: 1.2em, below: 1.2em)
|
||||||
|
#set heading(numbering: "1.1")
|
||||||
|
|
||||||
|
#show heading.where(level: 1): (it) => {
|
||||||
|
set text(size: larger-p )
|
||||||
|
set block(above: 1.2em, below: 1.2em)
|
||||||
|
if it.numbering != none {
|
||||||
|
let num = numbering(it.numbering, ..counter(heading).at(it.location()))
|
||||||
|
let prefix = num + h(0.5em) + text(code-border)[|] + h(0.5em)
|
||||||
|
unshift-prefix(prefix, it.body)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#show heading.where(level: 2): (it) => {
|
||||||
|
if it.numbering != none {
|
||||||
|
let num = numbering(it.numbering, ..counter(heading).at(it.location()))
|
||||||
|
unshift-prefix(num + h(0.8em), it.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// link color
|
||||||
|
#show link: it => text(fill:hei-blue, it)
|
||||||
|
|
||||||
|
// code blocks
|
||||||
|
#show raw: set text(
|
||||||
|
font: (
|
||||||
|
"Iosevka",
|
||||||
|
"Fira Code",
|
||||||
|
"JetBrains Mono",
|
||||||
|
"DejaVu Sans Mono",
|
||||||
|
),
|
||||||
|
fallback: true,)
|
||||||
|
#show raw.where(block: false): set text(weight: "semibold")
|
||||||
|
#show raw.where(block: true): set text(size: tiny)
|
||||||
|
#show raw.where(block: true): it => {
|
||||||
|
block(
|
||||||
|
fill: code-bg,
|
||||||
|
width:100%,
|
||||||
|
inset: 7pt,
|
||||||
|
radius: (left:0pt, right: 4pt),
|
||||||
|
stroke: (left: 3pt + luma(80%), rest: 0.1pt + code-border),
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#show: codly-init.with()
|
||||||
|
#codly(
|
||||||
|
display-icon: false,
|
||||||
|
languages: codly-languages,
|
||||||
|
zebra-fill: none,
|
||||||
|
stroke: 0.1pt + code-border,
|
||||||
|
radius: 4pt,
|
||||||
|
number-format: (number) => text(luma(210), size:7pt, [#h(1em)#number]),
|
||||||
|
inset: (left:-0.0em, rest:0.3em),
|
||||||
|
fill: code-bg,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Captions
|
||||||
|
#set figure(numbering: "1", supplement: get-supplement)
|
||||||
|
#set figure.caption(separator: " - ") // With a nice separator
|
||||||
|
#set math.equation(numbering: "(1)", supplement: i18n("equation-name"))
|
||||||
|
|
||||||
|
#show: word-count
|
||||||
|
|
||||||
|
// Function to create tag labels
|
||||||
|
#let badge(content) = {
|
||||||
|
let color = rgb("888888")
|
||||||
|
let textcolor = rgb("222222")
|
||||||
|
box(
|
||||||
|
inset: (x: 3pt, y: 2pt),
|
||||||
|
radius: 4pt,
|
||||||
|
fill: color.lighten(70%),
|
||||||
|
stroke: (paint: color, thickness: 0.5pt),
|
||||||
|
)[
|
||||||
|
#text(weight: "bold", size: 8pt, fill:textcolor)[#content]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show basic document metadata if front matter exists
|
||||||
|
#if has-frontmatter [
|
||||||
|
#if fm-title != none [
|
||||||
|
#align(center)[
|
||||||
|
#text(size: 18pt, weight: "bold")[#fm-title]
|
||||||
|
]
|
||||||
|
#v(0.3em)
|
||||||
|
]
|
||||||
|
#if fm-subtitle != none [
|
||||||
|
#align(center)[
|
||||||
|
#text(size: 14pt, style: "italic")[#fm-subtitle]
|
||||||
|
]
|
||||||
|
#v(0.3em)
|
||||||
|
]
|
||||||
|
#let metadata = ()
|
||||||
|
#if document-author != none { metadata.push(document-author) }
|
||||||
|
#if document-date != none { metadata.push(document-date.display()) }
|
||||||
|
#if fm_version != none { metadata.push(fm_version) }
|
||||||
|
#align(center)[
|
||||||
|
#for (i, data) in metadata.enumerate() [
|
||||||
|
#data
|
||||||
|
#if i < metadata.len() - 1 [ \- ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
#if fm-tags != none and tags-list.len() > 0 [
|
||||||
|
#align(center)[
|
||||||
|
#for (i, tag) in tags-list.enumerate() [
|
||||||
|
#badge(tag.trim())
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
#line(length: 100%, stroke: 0.5pt)
|
||||||
|
]
|
||||||
|
|
||||||
|
// Show table of contents if requested
|
||||||
|
#if show-toc [
|
||||||
|
#outline()
|
||||||
|
#pagebreak()
|
||||||
|
]
|
||||||
|
|
||||||
|
#cmarker.render(
|
||||||
|
read(filepath),
|
||||||
|
scope: (image: (path, alt: none) => image(path, alt: alt)),
|
||||||
|
math: mitex
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user