stamd

Static Markdown Page Generator
git clone git://git.dimitrijedobrota.com/stamd.git
Log | Files | Refs | README | LICENSE | HACKING | CONTRIBUTING | CODE_OF_CONDUCT | BUILDING |

commit99bcf0d96824df5e573ba76a2ac7c261133c596d
parent04e983cf30b9c6a9a45da5980e2354ade6ef6654
authorDimitrije Dobrota <mail@dimitrijedobrota.com>
dateMon, 24 Jun 2024 20:16:32 +0200

C++ rewrite * Use CMake for proper dependency handling * Use hemplate library for intuitive xml generation * More expresable and expandable codebase * Improved code safety

Diffstat:
M.clang-format|+++++++++++++++++++++++++++++++++++++++++-----------------------------------------
A.clang-tidy|+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.codespellrc|++++++
A.github/workflows/ci.yml|+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M.gitignore|+++++++++++----------------------------------------------------------
ABUILDING.md|++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACMakeLists.txt|+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACMakePresets.json|+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACODE_OF_CONDUCT.md|+++++
ACONTRIBUTING.md|++++++++++++++
AHACKING.md|+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE.md|+++++++++++++++++++++
DMakefile|---------------------------------------------------
MREADME.md|+++++++++++++++-
Dbin/dir.info|
Acmake/coverage.cmake|+++++++++++++++++++++++++++++++++
Acmake/dev-mode.cmake|+++++++++++++++++++++++
Acmake/folders.cmake|+++++++++++++++++++++
Acmake/install-rules.cmake|++++++++
Acmake/lint-targets.cmake|+++++++++++++++++++++++++++++++++
Acmake/lint.cmake|+++++++++++++++++++++++++++++++++++++++++++++++++++
Acmake/prelude.cmake|++++++++++
Acmake/project-is-top-level.cmake|++++++
Acmake/spell-targets.cmake|++++++++++++++++++++++
Acmake/spell.cmake|+++++++++++++++++++++++++++++
Acmake/variables.cmake|++++++++++++++++++++++++++++
Dinclude/dir.info|
Dobj/dir.info|
Asource/article.cpp|+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/article.hpp|+++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/main.cpp|+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/utility.hpp|++++++++++++++++++++++++++++++++++++
Dsrc/dir.info|
Dsrc/stamd.c|---------------------------------------------------------------------------------
Atest/CMakeLists.txt|+++++++++++++++++
Atest/source/stamd_test.cpp|++++

36 files changed, 1697 insertions(+), 808 deletions(-)


diff --git a/.clang-format b/.clang-format

@@ -1,178 +1,178 @@

---
Language: Cpp
# BasedOnStyle: LLVM
Language: Cpp
# BasedOnStyle: Chromium
AccessModifierOffset: -2
AlignAfterOpenBracket: true
AlignArrayOfStructures: Right
AlignConsecutiveMacros: true
AlignConsecutiveAssignments: None
AlignConsecutiveBitFields: None
AlignConsecutiveDeclarations: true
AlignAfterOpenBracket: Align
AlignConsecutiveMacros: false
AlignConsecutiveAssignments: true
AlignConsecutiveBitFields: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Right
AlignOperands: Align
AlignTrailingComments: true
AlignOperands: DontAlign
AlignTrailingComments: false
AllowAllArgumentsOnNextLine: true
AllowAllConstructorInitializersOnNextLine: true
AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortEnumsOnASingleLine: true
AllowShortBlocksOnASingleLine: true
AllowShortEnumsOnASingleLine: false
AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortFunctionsOnASingleLine: Inline
AllowShortLambdasOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AllowShortIfStatementsOnASingleLine: AllIfsAndElse
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: MultiLine
AttributeMacros:
- __capability
BinPackArguments: true
BinPackParameters: true
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: false
BinPackParameters: false
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterCaseLabel: false
AfterClass: true
AfterControlStatement: Always
AfterEnum: true
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
AfterStruct: true
AfterUnion: true
AfterExternBlock: true
BeforeCatch: false
BeforeElse: true
BeforeLambdaBody: true
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeConceptDeclarations: true
BreakBeforeBraces: Attach
BreakBeforeInheritanceComma: false
BreakInheritanceList: BeforeColon
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeBraces: Custom
# BreakBeforeInheritanceComma: true
BreakInheritanceList: BeforeComma
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakAfterJavaFieldAnnotations: false
BreakConstructorInitializersBeforeComma: true
BreakConstructorInitializers: BeforeComma
BreakAfterJavaFieldAnnotations: true
BreakStringLiterals: true
ColumnLimit: 80
CommentPragmas: '^ IWYU pragma:'
ColumnLimit: 79
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DeriveLineEnding: true
DeriveLineEnding: false
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: LogicalBlock
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IfMacros:
- KJ_IF_MAYBE
IncludeBlocks: Preserve
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
Priority: 2
SortPriority: 0
CaseSensitive: false
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3
SortPriority: 0
CaseSensitive: false
- Regex: '.*'
Priority: 1
SortPriority: 0
CaseSensitive: false
IncludeIsMainRegex: '(Test)?$'
# Standard library headers come before anything else
- Regex: '^<[a-z_]+>'
Priority: -1
- Regex: '^<.+\.h(pp)?>'
Priority: 1
- Regex: '^<.*'
Priority: 2
- Regex: '.*'
Priority: 3
IncludeIsMainRegex: ''
IncludeIsMainSourceRegex: ''
IndentAccessModifiers: false
IndentCaseLabels: false
IndentCaseLabels: true
IndentCaseBlocks: false
IndentGotoLabels: true
IndentPPDirectives: None
IndentExternBlock: AfterExternBlock
IndentRequires: false
IndentWidth: 2
IndentPPDirectives: AfterHash
IndentExternBlock: NoIndent
IndentWidth: 2
IndentWrappedFunctionNames: false
InsertTrailingCommas: None
JavaScriptQuotes: Leave
InsertTrailingCommas: Wrapped
JavaScriptQuotes: Double
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: true
LambdaBodyIndentation: Signature
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ''
MacroBlockEnd: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBinPackProtocolList: Never
ObjCBlockIndentWidth: 2
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakBeforeFirstCallParameter: 1
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PenaltyIndentedWhitespace: 0
PointerAlignment: Right
PPIndentWidth: -1
ReferenceAlignment: Pointer
ReflowComments: true
ShortNamespaceLines: 1
SortIncludes: CaseSensitive
SortJavaStaticImport: Before
PenaltyReturnTypeOnItsOwnLine: 200
PointerAlignment: Left
RawStringFormats:
- Language: Cpp
Delimiters:
- cc
- CC
- cpp
- Cpp
- CPP
- 'c++'
- 'C++'
CanonicalDelimiter: ''
BasedOnStyle: google
- Language: TextProto
Delimiters:
- pb
- PB
- proto
- PROTO
EnclosingFunctions:
- EqualsProto
- EquivToProto
- PARSE_PARTIAL_TEXT_PROTO
- PARSE_TEST_PROTO
- PARSE_TEXT_PROTO
- ParseTextOrDie
- ParseTextProtoOrDie
- ParseTestProto
- ParsePartialTestProto
CanonicalDelimiter: ''
BasedOnStyle: google
ReflowComments: true
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: true
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: false
SpaceBeforeCpp11BracedList: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceAroundPointerQualifiers: Default
SpaceBeforeParens: ControlStatementsExceptForEachMacros
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyBlock: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: Never
SpacesBeforeTrailingComments: 2
SpacesInAngles: false
SpacesInConditionalStatement: false
SpacesInContainerLiterals: true
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParentheses: false
SpacesInSquareBrackets: false
SpaceBeforeSquareBrackets: false
BitFieldColonSpacing: Both
Standard: Latest
StatementAttributeLikeMacros:
- Q_EMIT
Standard: Auto
StatementMacros:
- Q_UNUSED
- QT_REQUIRE_VERSION
TabWidth: 8
UseCRLF: false
UseTab: Never
TabWidth: 8
UseCRLF: false
UseTab: Never
WhitespaceSensitiveMacros:
- STRINGIZE
- PP_STRINGIZE
- BOOST_PP_STRINGIZE
- NS_SWIFT_NAME
- CF_SWIFT_NAME
...

diff --git a/.clang-tidy b/.clang-tidy

@@ -0,0 +1,165 @@

---
# Enable ALL the things! Except not really
# misc-non-private-member-variables-in-classes: the options don't do anything
# modernize-use-nodiscard: too aggressive, attribute is situationally useful
Checks: "*,\
-altera-*,\
-fuchsia-*,\
-llvmlibc-*,\
-*-braces-around-statements,\
-bugprone-argument-comment,\
-bugprone-easily-swappable-parameters,\
-cppcoreguidelines-avoid-magic-numbers,\
-hicpp-signed-bitwise,\
-llvm-header-guard,\
-llvm-include-order,\
-modernize-use-nodiscard,\
-modernize-use-trailing-return-type,\
-readability-function-cognitive-complexity,\
-readability-magic-numbers,\
fuchsia-multiple-inheritance,\
-misc-no-recursion,\
-misc-non-private-member-variables-in-classes"
WarningsAsErrors: ''
CheckOptions:
- key: 'bugprone-argument-comment.StrictMode'
value: 'true'
# Prefer using enum classes with 2 values for parameters instead of bools
- key: 'bugprone-argument-comment.CommentBoolLiterals'
value: 'true'
- key: 'bugprone-misplaced-widening-cast.CheckImplicitCasts'
value: 'true'
- key: 'bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression'
value: 'true'
- key: 'bugprone-suspicious-string-compare.WarnOnLogicalNotComparison'
value: 'true'
- key: 'readability-simplify-boolean-expr.ChainedConditionalReturn'
value: 'true'
- key: 'readability-simplify-boolean-expr.ChainedConditionalAssignment'
value: 'true'
- key: 'readability-uniqueptr-delete-release.PreferResetCall'
value: 'true'
- key: 'cppcoreguidelines-init-variables.MathHeader'
value: '<cmath>'
- key: 'cppcoreguidelines-narrowing-conversions.PedanticMode'
value: 'true'
- key: 'readability-else-after-return.WarnOnUnfixable'
value: 'true'
- key: 'readability-else-after-return.WarnOnConditionVariables'
value: 'true'
- key: 'readability-inconsistent-declaration-parameter-name.Strict'
value: 'true'
- key: 'readability-qualified-auto.AddConstToQualified'
value: 'true'
- key: 'readability-redundant-access-specifiers.CheckFirstDeclaration'
value: 'true'
# These seem to be the most common identifier styles
- key: 'readability-identifier-naming.AbstractClassCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ClassCase'
value: 'camelBack'
- key: 'readability-identifier-naming.ClassConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ClassMemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ClassMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstantMemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstantParameterCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstantPointerParameterCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstexprFunctionCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstexprMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ConstexprVariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.EnumCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.EnumConstantCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.FunctionCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalConstantPointerCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalFunctionCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalPointerCase'
value: 'lower_case'
- key: 'readability-identifier-naming.GlobalVariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.InlineNamespaceCase'
value: 'lower_case'
- key: 'readability-identifier-naming.LocalConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.LocalConstantPointerCase'
value: 'lower_case'
- key: 'readability-identifier-naming.LocalPointerCase'
value: 'lower_case'
- key: 'readability-identifier-naming.LocalVariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.MacroDefinitionCase'
value: 'UPPER_CASE'
- key: 'readability-identifier-naming.MemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.MethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.NamespaceCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ParameterCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ParameterPackCase'
value: 'lower_case'
- key: 'readability-identifier-naming.PointerParameterCase'
value: 'lower_case'
- key: 'readability-identifier-naming.PrivateMemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.PrivateMemberPrefix'
value: 'm_'
- key: 'readability-identifier-naming.PrivateMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ProtectedMemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ProtectedMemberPrefix'
value: 'm_'
- key: 'readability-identifier-naming.ProtectedMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.PublicMemberCase'
value: 'lower_case'
- key: 'readability-identifier-naming.PublicMethodCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ScopedEnumConstantCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.StaticConstantCase'
value: 'lower_case'
- key: 'readability-identifier-naming.StaticVariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.StructCase'
value: 'lower_case'
- key: 'readability-identifier-naming.TemplateParameterCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.TemplateTemplateParameterCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.TypeAliasCase'
value: 'lower_case'
- key: 'readability-identifier-naming.TypedefCase'
value: 'lower_case'
- key: 'readability-identifier-naming.TypeTemplateParameterCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.TypeTemplateParameterIgnoredRegexp'
value: 'expr-type'
- key: 'readability-identifier-naming.UnionCase'
value: 'lower_case'
- key: 'readability-identifier-naming.ValueTemplateParameterCase'
value: 'CamelCase'
- key: 'readability-identifier-naming.VariableCase'
value: 'lower_case'
- key: 'readability-identifier-naming.VirtualMethodCase'
value: 'lower_case'
...

diff --git a/.codespellrc b/.codespellrc

@@ -0,0 +1,6 @@

[codespell]
builtin = clear,rare,en-GB_to_en-US,names,informal,code
check-filenames =
check-hidden =
skip = */.git,*/build,*/prefix
quiet-level = 2

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml

@@ -0,0 +1,186 @@

name: Continuous Integration
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- name: Install codespell
run: pip3 install codespell
- name: Lint
run: cmake -D FORMAT_COMMAND=clang-format-14 -P cmake/lint.cmake
- name: Spell check
if: always()
run: cmake -P cmake/spell.cmake
coverage:
needs: [lint]
runs-on: ubuntu-22.04
# To enable coverage, delete the last line from the conditional below and
# edit the "<name>" placeholder to your GitHub name.
# If you do not wish to use codecov, then simply delete this job from the
# workflow.
if: github.repository_owner == '<name>'
&& false
steps:
- uses: actions/checkout@v4
- name: Install LCov
run: sudo apt-get update -q
&& sudo apt-get install lcov -q -y
- name: Configure
run: cmake --preset=ci-coverage
- name: Build
run: cmake --build build/coverage -j 2
- name: Test
working-directory: build/coverage
run: ctest --output-on-failure --no-tests=error -j 2
- name: Process coverage info
run: cmake --build build/coverage -t coverage
- name: Submit to codecov.io
uses: codecov/codecov-action@v4
with:
file: build/coverage/coverage.info
token: ${{ secrets.CODECOV_TOKEN }}
sanitize:
needs: [lint]
runs-on: ubuntu-22.04
env: { CXX: clang++-14 }
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake --preset=ci-sanitize
- name: Build
run: cmake --build build/sanitize -j 2
- name: Test
working-directory: build/sanitize
env:
ASAN_OPTIONS: "strict_string_checks=1:\
detect_stack_use_after_return=1:\
check_initialization_order=1:\
strict_init_order=1:\
detect_leaks=1:\
halt_on_error=1"
UBSAN_OPTIONS: "print_stacktrace=1:\
halt_on_error=1"
run: ctest --output-on-failure --no-tests=error -j 2
test:
needs: [lint]
strategy:
matrix:
os: [macos-14, ubuntu-22.04, windows-2022]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install static analyzers
if: matrix.os == 'ubuntu-22.04'
run: >-
sudo apt-get install clang-tidy-14 cppcheck -y -q
sudo update-alternatives --install
/usr/bin/clang-tidy clang-tidy
/usr/bin/clang-tidy-14 140
- name: Setup MultiToolTask
if: matrix.os == 'windows-2022'
run: |
Add-Content "$env:GITHUB_ENV" 'UseMultiToolTask=true'
Add-Content "$env:GITHUB_ENV" 'EnforceProcessCountAcrossBuilds=true'
- name: Configure
shell: pwsh
run: cmake "--preset=ci-$("${{ matrix.os }}".split("-")[0])"
- name: Build
run: cmake --build build --config Release -j 2
- name: Install
run: cmake --install build --config Release --prefix prefix
- name: Test
working-directory: build
run: ctest --output-on-failure --no-tests=error -C Release -j 2
docs:
# Deploy docs only when builds succeed
needs: [sanitize, test]
runs-on: ubuntu-22.04
# To enable, first you have to create an orphaned gh-pages branch:
#
# git switch --orphan gh-pages
# git commit --allow-empty -m "Initial commit"
# git push -u origin gh-pages
#
# Edit the <name> placeholder below to your GitHub name, so this action
# runs only in your repository and no one else's fork. After these, delete
# this comment and the last line in the conditional below.
# If you do not wish to use GitHub Pages for deploying documentation, then
# simply delete this job similarly to the coverage one.
if: github.ref == 'refs/heads/master'
&& github.event_name == 'push'
&& github.repository_owner == '<name>'
&& false
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- name: Install m.css dependencies
run: pip3 install jinja2 Pygments
- name: Install Doxygen
run: sudo apt-get update -q
&& sudo apt-get install doxygen -q -y
- name: Build docs
run: cmake "-DPROJECT_SOURCE_DIR=$PWD" "-DPROJECT_BINARY_DIR=$PWD/build"
-P cmake/docs-ci.cmake
- name: Deploy docs
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: build/docs/html

diff --git a/.gitignore b/.gitignore

@@ -1,58 +1,11 @@

.ccls-cache/
.ccls
docs/*
bin/*
!bin/dir.info
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
**/.DS_Store
.idea/
.vs/
.vscode/
build/
cmake-build-*/
prefix/
.clangd
CMakeLists.txt.user
CMakeUserPresets.json
compile_commands.json

diff --git a/BUILDING.md b/BUILDING.md

@@ -0,0 +1,60 @@

# Building with CMake
## Build
This project doesn't require any special command-line flags to build to keep
things simple.
Here are the steps for building in release mode with a single-configuration
generator, like the Unix Makefiles one:
```sh
cmake -S . -B build -D CMAKE_BUILD_TYPE=Release
cmake --build build
```
Here are the steps for building in release mode with a multi-configuration
generator, like the Visual Studio ones:
```sh
cmake -S . -B build
cmake --build build --config Release
```
### Building with MSVC
Note that MSVC by default is not standards compliant and you need to pass some
flags to make it behave properly. See the `flags-msvc` preset in the
[CMakePresets.json](CMakePresets.json) file for the flags and with what
variable to provide them to CMake during configuration.
### Building on Apple Silicon
CMake supports building on Apple Silicon properly since 3.20.1. Make sure you
have the [latest version][1] installed.
## Install
This project doesn't require any special command-line flags to install to keep
things simple. As a prerequisite, the project has to be built with the above
commands already.
The below commands require at least CMake 3.15 to run, because that is the
version in which [Install a Project][2] was added.
Here is the command for installing the release mode artifacts with a
single-configuration generator, like the Unix Makefiles one:
```sh
cmake --install build
```
Here is the command for installing the release mode artifacts with a
multi-configuration generator, like the Visual Studio ones:
```sh
cmake --install build --config Release
```
[1]: https://cmake.org/download/
[2]: https://cmake.org/cmake/help/latest/manual/cmake.1.html#install-a-project

diff --git a/CMakeLists.txt b/CMakeLists.txt

@@ -0,0 +1,75 @@

cmake_minimum_required(VERSION 3.24)
include(cmake/prelude.cmake)
project(
stamd
VERSION 0.2.0
DESCRIPTION "Static Markdown Page Generator"
HOMEPAGE_URL "https://git.dimitrijedobrota.com/stamd.git"
LANGUAGES CXX
)
include(cmake/project-is-top-level.cmake)
include(cmake/variables.cmake)
# ---- Declare dependencies ----
find_package(poafloc 1 CONFIG REQUIRED)
find_package(hemplate 0.1 CONFIG REQUIRED)
include(FetchContent)
FetchContent_Declare(
maddy
URL https://github.com/progsource/maddy/releases/download/1.3.0/maddy-src.zip
)
FetchContent_MakeAvailable(maddy)
# ---- Declare library ----
add_library(
stamd_lib OBJECT
source/article.cpp
)
target_link_libraries(stamd_lib PUBLIC hemplate::hemplate)
target_include_directories(
stamd_lib ${warning_guard}
PUBLIC
"\$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/source>"
)
target_compile_features(stamd_lib PUBLIC cxx_std_20)
# ---- Declare executable ----
add_executable(stamd_exe source/main.cpp)
add_executable(stamd::exe ALIAS stamd_exe)
set_property(TARGET stamd_exe PROPERTY OUTPUT_NAME stamd)
target_compile_features(stamd_exe PRIVATE cxx_std_20)
target_link_libraries(stamd_exe PRIVATE poafloc maddy stamd_lib)
# ---- Install rules ----
if(NOT CMAKE_SKIP_INSTALL_RULES)
include(cmake/install-rules.cmake)
endif()
# ---- Developer mode ----
if(NOT stamd_DEVELOPER_MODE)
return()
elseif(NOT PROJECT_IS_TOP_LEVEL)
message(
AUTHOR_WARNING
"Developer mode is intended for developers of stamd"
)
endif()
include(cmake/dev-mode.cmake)

diff --git a/CMakePresets.json b/CMakePresets.json

@@ -0,0 +1,159 @@

{
"version": 2,
"cmakeMinimumRequired": {
"major": 3,
"minor": 14,
"patch": 0
},
"configurePresets": [
{
"name": "cmake-pedantic",
"hidden": true,
"warnings": {
"dev": true,
"deprecated": true,
"uninitialized": true,
"unusedCli": true,
"systemVars": false
},
"errors": {
"dev": true,
"deprecated": true
}
},
{
"name": "dev-mode",
"hidden": true,
"inherits": "cmake-pedantic",
"cacheVariables": {
"stamd_DEVELOPER_MODE": "ON"
}
},
{
"name": "cppcheck",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_CPPCHECK": "cppcheck;--inline-suppr"
}
},
{
"name": "clang-tidy",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=^${sourceDir}/"
}
},
{
"name": "ci-std",
"description": "This preset makes sure the project actually builds with at least the specified standard",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_EXTENSIONS": "OFF",
"CMAKE_CXX_STANDARD": "20",
"CMAKE_CXX_STANDARD_REQUIRED": "ON"
}
},
{
"name": "flags-gcc-clang",
"description": "These flags are supported by both GCC and Clang",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_FLAGS": "-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS=1 -fstack-protector-strong -fcf-protection=full -fstack-clash-protection -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat=2 -Wundef -Werror=float-equal -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast",
"CMAKE_EXE_LINKER_FLAGS": "-Wl,--allow-shlib-undefined,--as-needed,-z,noexecstack,-z,relro,-z,now,-z,nodlopen",
"CMAKE_SHARED_LINKER_FLAGS": "-Wl,--allow-shlib-undefined,--as-needed,-z,noexecstack,-z,relro,-z,now,-z,nodlopen"
}
},
{
"name": "flags-appleclang",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_FLAGS": "-fstack-protector-strong -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat=2 -Wundef -Werror=float-equal -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast"
}
},
{
"name": "flags-msvc",
"description": "Note that all the flags after /W4 are required for MSVC to conform to the language standard",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_FLAGS": "/sdl /guard:cf /utf-8 /diagnostics:caret /w14165 /w44242 /w44254 /w44263 /w34265 /w34287 /w44296 /w44365 /w44388 /w44464 /w14545 /w14546 /w14547 /w14549 /w14555 /w34619 /w34640 /w24826 /w14905 /w14906 /w14928 /w45038 /W4 /permissive- /volatile:iso /Zc:inline /Zc:preprocessor /Zc:enumTypes /Zc:lambda /Zc:__cplusplus /Zc:externConstexpr /Zc:throwingNew /EHsc",
"CMAKE_EXE_LINKER_FLAGS": "/machine:x64 /guard:cf"
}
},
{
"name": "ci-linux",
"inherits": ["flags-gcc-clang", "ci-std"],
"generator": "Unix Makefiles",
"hidden": true,
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
},
{
"name": "ci-darwin",
"inherits": ["flags-appleclang", "ci-std"],
"generator": "Xcode",
"hidden": true
},
{
"name": "ci-win64",
"inherits": ["flags-msvc", "ci-std"],
"generator": "Visual Studio 17 2022",
"architecture": "x64",
"hidden": true
},
{
"name": "coverage-linux",
"binaryDir": "${sourceDir}/build/coverage",
"inherits": "ci-linux",
"hidden": true,
"cacheVariables": {
"ENABLE_COVERAGE": "ON",
"CMAKE_BUILD_TYPE": "Coverage",
"CMAKE_CXX_FLAGS_COVERAGE": "-Og -g --coverage -fkeep-inline-functions -fkeep-static-functions",
"CMAKE_EXE_LINKER_FLAGS_COVERAGE": "--coverage",
"CMAKE_SHARED_LINKER_FLAGS_COVERAGE": "--coverage"
}
},
{
"name": "ci-coverage",
"inherits": ["coverage-linux", "dev-mode"],
"cacheVariables": {
"COVERAGE_HTML_COMMAND": ""
}
},
{
"name": "ci-sanitize",
"binaryDir": "${sourceDir}/build/sanitize",
"inherits": ["ci-linux", "dev-mode"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Sanitize",
"CMAKE_CXX_FLAGS_SANITIZE": "-U_FORTIFY_SOURCE -O2 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-common"
}
},
{
"name": "ci-build",
"binaryDir": "${sourceDir}/build",
"hidden": true
},
{
"name": "ci-multi-config",
"description": "Speed up multi-config generators by generating only one configuration instead of the defaults",
"hidden": true,
"cacheVariables": {
"CMAKE_CONFIGURATION_TYPES": "Release"
}
},
{
"name": "ci-macos",
"inherits": ["ci-build", "ci-darwin", "dev-mode", "ci-multi-config"]
},
{
"name": "ci-ubuntu",
"inherits": ["ci-build", "ci-linux", "clang-tidy", "cppcheck", "dev-mode"]
},
{
"name": "ci-windows",
"inherits": ["ci-build", "ci-win64", "dev-mode", "ci-multi-config"]
}
]
}

diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md

@@ -0,0 +1,5 @@

# Code of Conduct
* You will be judged by your contributions first, and your sense of humor
second.
* Nobody owes you anything.

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md

@@ -0,0 +1,14 @@

# Contributing
## Code of Conduct
Please see the [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) document.
## Getting started
Helpful notes for developers can be found in the [`HACKING.md`](HACKING.md)
document.
In addition to he above, if you use the presets file as instructed, then you
should NOT check it into source control, just as the CMake documentation
suggests.

diff --git a/HACKING.md b/HACKING.md

@@ -0,0 +1,149 @@

# Hacking
Here is some wisdom to help you build and test this project as a developer and
potential contributor.
If you plan to contribute, please read the [CONTRIBUTING](CONTRIBUTING.md)
guide.
## Developer mode
Build system targets that are only useful for developers of this project are
hidden if the `stamd_DEVELOPER_MODE` option is disabled. Enabling this
option makes tests and other developer targets and options available. Not
enabling this option means that you are a consumer of this project and thus you
have no need for these targets and options.
Developer mode is always set to on in CI workflows.
### Presets
This project makes use of [presets][1] to simplify the process of configuring
the project. As a developer, you are recommended to always have the [latest
CMake version][2] installed to make use of the latest Quality-of-Life
additions.
You have a few options to pass `stamd_DEVELOPER_MODE` to the configure
command, but this project prefers to use presets.
As a developer, you should create a `CMakeUserPresets.json` file at the root of
the project:
```json
{
"version": 2,
"cmakeMinimumRequired": {
"major": 3,
"minor": 14,
"patch": 0
},
"configurePresets": [
{
"name": "dev",
"binaryDir": "${sourceDir}/build/dev",
"inherits": ["dev-mode", "ci-<os>"],
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
}
],
"buildPresets": [
{
"name": "dev",
"configurePreset": "dev",
"configuration": "Debug"
}
],
"testPresets": [
{
"name": "dev",
"configurePreset": "dev",
"configuration": "Debug",
"output": {
"outputOnFailure": true
}
}
]
}
```
You should replace `<os>` in your newly created presets file with the name of
the operating system you have, which may be `win64`, `linux` or `darwin`. You
can see what these correspond to in the
[`CMakePresets.json`](CMakePresets.json) file.
`CMakeUserPresets.json` is also the perfect place in which you can put all
sorts of things that you would otherwise want to pass to the configure command
in the terminal.
> **Note**
> Some editors are pretty greedy with how they open projects with presets.
> Some just randomly pick a preset and start configuring without your consent,
> which can be confusing. Make sure that your editor configures when you
> actually want it to, for example in CLion you have to make sure only the
> `dev-dev preset` has `Enable profile` ticked in
> `File > Settings... > Build, Execution, Deployment > CMake` and in Visual
> Studio you have to set the option `Never run configure step automatically`
> in `Tools > Options > CMake` **prior to opening the project**, after which
> you can manually configure using `Project > Configure Cache`.
### Configure, build and test
If you followed the above instructions, then you can configure, build and test
the project respectively with the following commands from the project root on
any operating system with any build system:
```sh
cmake --preset=dev
cmake --build --preset=dev
ctest --preset=dev
```
If you are using a compatible editor (e.g. VSCode) or IDE (e.g. CLion, VS), you
will also be able to select the above created user presets for automatic
integration.
Please note that both the build and test commands accept a `-j` flag to specify
the number of jobs to use, which should ideally be specified to the number of
threads your CPU has. You may also want to add that to your preset using the
`jobs` property, see the [presets documentation][1] for more details.
### Developer mode targets
These are targets you may invoke using the build command from above, with an
additional `-t <target>` flag:
#### `coverage`
Available if `ENABLE_COVERAGE` is enabled. This target processes the output of
the previously run tests when built with coverage configuration. The commands
this target runs can be found in the `COVERAGE_TRACE_COMMAND` and
`COVERAGE_HTML_COMMAND` cache variables. The trace command produces an info
file by default, which can be submitted to services with CI integration. The
HTML command uses the trace command's output to generate an HTML document to
`<binary-dir>/coverage_html` by default.
#### `docs`
Available if `BUILD_MCSS_DOCS` is enabled. Builds to documentation using
Doxygen and m.css. The output will go to `<binary-dir>/docs` by default
(customizable using `DOXYGEN_OUTPUT_DIRECTORY`).
#### `format-check` and `format-fix`
These targets run the clang-format tool on the codebase to check errors and to
fix them respectively. Customization available using the `FORMAT_PATTERNS` and
`FORMAT_COMMAND` cache variables.
#### `run-exe`
Runs the executable target `stamd_exe`.
#### `spell-check` and `spell-fix`
These targets run the codespell tool on the codebase to check errors and to fix
them respectively. Customization available using the `SPELL_COMMAND` cache
variable.
[1]: https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html
[2]: https://cmake.org/download/

diff --git a/LICENSE.md b/LICENSE.md

@@ -0,0 +1,21 @@

MIT License
Copyright (c) 2022-2024 Dimitrije Dobrota
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

diff --git a/Makefile b/Makefile

@@ -1,51 +0,0 @@

# GNU Makefile for Game of Life simulation
#
# Usage: make [-f path\Makefile] [DEBUG=Y] target
NAME = stamd
CC = gcc
CFLAGS = -Iinclude
LDFLAGS += -lmd4c-html
SRC = src
OBJ = obj
BINDIR = bin
BIN = bin/$(NAME)
SRCS=$(wildcard $(SRC)/*.c)
OBJS=$(patsubst $(SRC)/%.c, $(OBJ)/%.o, $(SRCS))
ifeq ($(DEBUG),Y)
CFLAGS += -lciid
CFLAGS += -Wall -ggdb
else
CFLAGS += -lcii
endif
all: $(BIN)
$(BIN): $(OBJS)
$(CC) $^ $(CFLAGS) $(LDFLAGS) -o $@
$(OBJ)/%.o: $(SRC)/%.c
$(CC) -c $< -o $@ $(CFLAGS) $(LDFLAGS)
clean:
-$(RM) $(BIN) $(OBJS)
help:
@echo "Stamd - Static Markdown Page Generator"
@echo
@echo "Usage: make [-f path\Makefile] [DEBUG=Y] target"
@echo
@echo "Target rules:"
@echo " all - Compiles binary file [Default]"
@echo " clean - Clean the project by removing binaries"
@echo " help - Prints a help message with target rules"
@echo
@echo "Optional parameters:"
@echo " DEBUG - Compile binary file with debug flags enabled"
@echo
.PHONY: all clean help docs

diff --git a/README.md b/README.md

@@ -1,1 +1,15 @@

# Stamd - Static Markdown Page Generator
# stamd
This is the stamd project.
# Building and installing
See the [BUILDING](BUILDING.md) document.
# Contributing
See the [CONTRIBUTING](CONTRIBUTING.md) document.
# Licensing
This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) document for details

diff --git a/bin/dir.info b/bin/dir.info

diff --git a/cmake/coverage.cmake b/cmake/coverage.cmake

@@ -0,0 +1,33 @@

# ---- Variables ----
# We use variables separate from what CTest uses, because those have
# customization issues
set(
COVERAGE_TRACE_COMMAND
lcov -c -q
-o "${PROJECT_BINARY_DIR}/coverage.info"
-d "${PROJECT_BINARY_DIR}"
--include "${PROJECT_SOURCE_DIR}/*"
CACHE STRING
"; separated command to generate a trace for the 'coverage' target"
)
set(
COVERAGE_HTML_COMMAND
genhtml --legend -f -q
"${PROJECT_BINARY_DIR}/coverage.info"
-p "${PROJECT_SOURCE_DIR}"
-o "${PROJECT_BINARY_DIR}/coverage_html"
CACHE STRING
"; separated command to generate an HTML report for the 'coverage' target"
)
# ---- Coverage target ----
add_custom_target(
coverage
COMMAND ${COVERAGE_TRACE_COMMAND}
COMMAND ${COVERAGE_HTML_COMMAND}
COMMENT "Generating coverage report"
VERBATIM
)

diff --git a/cmake/dev-mode.cmake b/cmake/dev-mode.cmake

@@ -0,0 +1,23 @@

include(cmake/folders.cmake)
include(CTest)
if(BUILD_TESTING)
add_subdirectory(test)
endif()
add_custom_target(
run-exe
COMMAND stamd_exe
VERBATIM
)
add_dependencies(run-exe stamd_exe)
option(ENABLE_COVERAGE "Enable coverage support separate from CTest's" OFF)
if(ENABLE_COVERAGE)
include(cmake/coverage.cmake)
endif()
include(cmake/lint-targets.cmake)
include(cmake/spell-targets.cmake)
add_folders(Project)

diff --git a/cmake/folders.cmake b/cmake/folders.cmake

@@ -0,0 +1,21 @@

set_property(GLOBAL PROPERTY USE_FOLDERS YES)
# Call this function at the end of a directory scope to assign a folder to
# targets created in that directory. Utility targets will be assigned to the
# UtilityTargets folder, otherwise to the ${name}Targets folder. If a target
# already has a folder assigned, then that target will be skipped.
function(add_folders name)
get_property(targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
foreach(target IN LISTS targets)
get_property(folder TARGET "${target}" PROPERTY FOLDER)
if(DEFINED folder)
continue()
endif()
set(folder Utility)
get_property(type TARGET "${target}" PROPERTY TYPE)
if(NOT type STREQUAL "UTILITY")
set(folder "${name}")
endif()
set_property(TARGET "${target}" PROPERTY FOLDER "${folder}Targets")
endforeach()
endfunction()

diff --git a/cmake/install-rules.cmake b/cmake/install-rules.cmake

@@ -0,0 +1,8 @@

install(
TARGETS stamd_exe
RUNTIME COMPONENT stamd_Runtime
)
if(PROJECT_IS_TOP_LEVEL)
include(CPack)
endif()

diff --git a/cmake/lint-targets.cmake b/cmake/lint-targets.cmake

@@ -0,0 +1,33 @@

set(
FORMAT_PATTERNS
source/*.cpp source/*.hpp
include/*.hpp
test/*.cpp test/*.hpp
CACHE STRING
"; separated patterns relative to the project source dir to format"
)
set(FORMAT_COMMAND clang-format CACHE STRING "Formatter to use")
add_custom_target(
format-check
COMMAND "${CMAKE_COMMAND}"
-D "FORMAT_COMMAND=${FORMAT_COMMAND}"
-D "PATTERNS=${FORMAT_PATTERNS}"
-P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
COMMENT "Linting the code"
VERBATIM
)
add_custom_target(
format-fix
COMMAND "${CMAKE_COMMAND}"
-D "FORMAT_COMMAND=${FORMAT_COMMAND}"
-D "PATTERNS=${FORMAT_PATTERNS}"
-D FIX=YES
-P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
COMMENT "Fixing the code"
VERBATIM
)

diff --git a/cmake/lint.cmake b/cmake/lint.cmake

@@ -0,0 +1,51 @@

cmake_minimum_required(VERSION 3.14)
macro(default name)
if(NOT DEFINED "${name}")
set("${name}" "${ARGN}")
endif()
endmacro()
default(FORMAT_COMMAND clang-format)
default(
PATTERNS
source/*.cpp source/*.hpp
include/*.hpp
test/*.cpp test/*.hpp
)
default(FIX NO)
set(flag --output-replacements-xml)
set(args OUTPUT_VARIABLE output)
if(FIX)
set(flag -i)
set(args "")
endif()
file(GLOB_RECURSE files ${PATTERNS})
set(badly_formatted "")
set(output "")
string(LENGTH "${CMAKE_SOURCE_DIR}/" path_prefix_length)
foreach(file IN LISTS files)
execute_process(
COMMAND "${FORMAT_COMMAND}" --style=file "${flag}" "${file}"
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
RESULT_VARIABLE result
${args}
)
if(NOT result EQUAL "0")
message(FATAL_ERROR "'${file}': formatter returned with ${result}")
endif()
if(NOT FIX AND output MATCHES "\n<replacement offset")
string(SUBSTRING "${file}" "${path_prefix_length}" -1 relative_file)
list(APPEND badly_formatted "${relative_file}")
endif()
set(output "")
endforeach()
if(NOT badly_formatted STREQUAL "")
list(JOIN badly_formatted "\n" bad_list)
message("The following files are badly formatted:\n\n${bad_list}\n")
message(FATAL_ERROR "Run again with FIX=YES to fix these files.")
endif()

diff --git a/cmake/prelude.cmake b/cmake/prelude.cmake

@@ -0,0 +1,10 @@

# ---- In-source guard ----
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
message(
FATAL_ERROR
"In-source builds are not supported. "
"Please read the BUILDING document before trying to build this project. "
"You may need to delete 'CMakeCache.txt' and 'CMakeFiles/' first."
)
endif()

diff --git a/cmake/project-is-top-level.cmake b/cmake/project-is-top-level.cmake

@@ -0,0 +1,6 @@

# This variable is set by project() in CMake 3.21+
string(
COMPARE EQUAL
"${CMAKE_SOURCE_DIR}" "${PROJECT_SOURCE_DIR}"
PROJECT_IS_TOP_LEVEL
)

diff --git a/cmake/spell-targets.cmake b/cmake/spell-targets.cmake

@@ -0,0 +1,22 @@

set(SPELL_COMMAND codespell CACHE STRING "Spell checker to use")
add_custom_target(
spell-check
COMMAND "${CMAKE_COMMAND}"
-D "SPELL_COMMAND=${SPELL_COMMAND}"
-P "${PROJECT_SOURCE_DIR}/cmake/spell.cmake"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
COMMENT "Checking spelling"
VERBATIM
)
add_custom_target(
spell-fix
COMMAND "${CMAKE_COMMAND}"
-D "SPELL_COMMAND=${SPELL_COMMAND}"
-D FIX=YES
-P "${PROJECT_SOURCE_DIR}/cmake/spell.cmake"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
COMMENT "Fixing spelling errors"
VERBATIM
)

diff --git a/cmake/spell.cmake b/cmake/spell.cmake

@@ -0,0 +1,29 @@

cmake_minimum_required(VERSION 3.14)
macro(default name)
if(NOT DEFINED "${name}")
set("${name}" "${ARGN}")
endif()
endmacro()
default(SPELL_COMMAND codespell)
default(FIX NO)
set(flag "")
if(FIX)
set(flag -w)
endif()
execute_process(
COMMAND "${SPELL_COMMAND}" ${flag}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
RESULT_VARIABLE result
)
if(result EQUAL "65")
message(FATAL_ERROR "Run again with FIX=YES to fix these errors.")
elseif(result EQUAL "64")
message(FATAL_ERROR "Spell checker printed the usage info. Bad arguments?")
elseif(NOT result EQUAL "0")
message(FATAL_ERROR "Spell checker returned with ${result}")
endif()

diff --git a/cmake/variables.cmake b/cmake/variables.cmake

@@ -0,0 +1,28 @@

# ---- Developer mode ----
# Developer mode enables targets and code paths in the CMake scripts that are
# only relevant for the developer(s) of stamd
# Targets necessary to build the project must be provided unconditionally, so
# consumers can trivially build and package the project
if(PROJECT_IS_TOP_LEVEL)
option(stamd_DEVELOPER_MODE "Enable developer mode" OFF)
endif()
# ---- Warning guard ----
# target_include_directories with the SYSTEM modifier will request the compiler
# to omit warnings from the provided paths, if the compiler supports that
# This is to provide a user experience similar to find_package when
# add_subdirectory or FetchContent is used to consume this project
set(warning_guard "")
if(NOT PROJECT_IS_TOP_LEVEL)
option(
stamd_INCLUDES_WITH_SYSTEM
"Use SYSTEM modifier for stamd's includes, disabling warnings"
ON
)
mark_as_advanced(stamd_INCLUDES_WITH_SYSTEM)
if(stamd_INCLUDES_WITH_SYSTEM)
set(warning_guard SYSTEM)
endif()
endif()

diff --git a/include/dir.info b/include/dir.info

diff --git a/obj/dir.info b/obj/dir.info

diff --git a/source/article.cpp b/source/article.cpp

@@ -0,0 +1,103 @@

#include <format>
#include <numeric>
#include "article.hpp"
#include <hemplate/attribute.hpp>
#include <hemplate/classes.hpp>
#include "utility.hpp"
void article::print_nav(std::ostream& ost)
{
using namespace hemplate; // NOLINT
static const char* base = "https://dimitrijedobrota.com/blog";
ost << html::div()
.add(html::nav()
.add(html::a("&lt;-- back", {{"class", "back"}}))
.add(html::a("index", {{"href", base}}))
.add(html::a("hime --&gt;", {{"href", "/"}})))
.add(html::hr());
}
void article::print_categories(std::ostream& ost,
const categories_t& categories)
{
using namespace hemplate; // NOLINT
ost << html::div(
attributeList({{"class", "categories"}}),
std::accumulate(
begin(categories),
end(categories),
elementList(html::h3("Categories: "), html::p()),
[](elementList&& list, std::string ctgry)
{
normalize(ctgry);
list.add(
html::a(ctgry, {{"href", std::format("./{}.html", ctgry)}}));
return std::move(list);
})
.add(html::p()));
}
void article::write(const std::string& data, std::ostream& ost)
{
using namespace hemplate; // NOLINT
static const char* description_s =
"Dimitrije Dobrota's personal site. You can find my daily findings in a "
"form of articles on my blog as well as various programming projects.";
static const attributeList icon = {{"rel", "icon"}, {"type", "image/png"}};
static const attributeList style = {{"rel", "stylesheet"},
{"type", "text/css"}};
static const attributeList viewport = {
{"content", "width=device-width, initial-scale=1"},
{"name", "viewport"}};
static const attributeList description = {{"name", "description"},
{"content", description_s}};
ost << html::html().set("lang", get_language());
ost << html::head()
.add(html::title(get_title()))
.add(html::meta(viewport))
.add(html::meta({{"charset", "UTF-8"}}))
.add(html::meta(description))
.add(html::link(style).set("href", "/css/index.css"))
.add(html::link(style).set("href", "/css/colors.css"))
.add(html::link(icon)
.set("sizes", "32x32")
.set("href", "/img/favicon-32x32.png"))
.add(html::link(icon)
.set("sizes", "16x16")
.set("href", "/img/favicon-16x16.png"));
ost << html::body();
ost << html::input()
.set("type", "checkbox")
.set("id", "theme_switch")
.set("class", "theme_switch");
ost << html::main();
ost << html::div().set("class", "content");
ost << html::label(" ")
.set("for", "theme_switch")
.set("class", "switch_label");
if (!m_nonav) print_nav(ost);
if (!m_categories.empty()) print_categories(ost, m_categories);
ost << data;
if (!m_nonav) print_nav(ost);
ost << html::div();
ost << html::main();
ost << html::script(" ").set("source", "/scripts/main.js");
ost << html::body();
ost << html::html();
}

diff --git a/source/article.hpp b/source/article.hpp

@@ -0,0 +1,51 @@

#pragma once
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
class article
{
public:
using symbols_t = std::unordered_map<std::string, std::string>;
using categories_t = std::set<std::string>;
explicit article(std::string name, categories_t categories = {})
: m_name(std::move(name))
, m_categories(std::move(categories))
{
}
void write(const std::string& data, std::ostream& ost);
void emplace(const std::string& category) { m_categories.emplace(category); }
void emplace(const std::string& key, const std::string& value)
{
m_symbols.emplace(key, value);
}
auto get_categories() const { return m_categories; }
void set_hidden(bool state) { m_hidden = state; }
void set_nonav(bool state) { m_nonav = state; }
bool is_hidden() const { return m_hidden; }
const std::string& get_language() { return m_symbols.find("lang")->second; }
const std::string& get_title() { return m_symbols.find("title")->second; }
const std::string& get_date() { return m_symbols.find("date")->second; }
private:
static void print_nav(std::ostream& ost);
static void print_categories(std::ostream& ost,
const categories_t& categories);
std::string m_name;
bool m_hidden = false;
bool m_nonav = false;
categories_t m_categories;
symbols_t m_symbols = {
{"title", "test"}, {"lang", "en"}, {"date", "1970-01-01"}};
};

diff --git a/source/main.cpp b/source/main.cpp

@@ -0,0 +1,247 @@

#include <algorithm>
#include <format>
#include <fstream>
#include <iostream>
#include <memory>
#include <numeric>
#include <sstream>
#include <hemplate/attribute.hpp>
#include <hemplate/classes.hpp>
#include <poafloc/poafloc.hpp>
#include "article.hpp"
#include "maddy/parser.h"
#include "utility.hpp"
using article_list = std::vector<std::shared_ptr<article>>;
using categories_t = article::categories_t;
void preprocess(article& article, std::istream& ist)
{
std::string line;
std::string key;
std::string value;
while (std::getline(ist, line))
{
if (line.empty()) continue;
if (line[0] != '@') break;
{
std::istringstream iss(line.substr(1));
std::getline(iss, key, ':');
std::getline(iss, value);
trim(key);
trim(value);
}
if (key == "hidden") article.set_hidden(true);
else if (key == "nonav") article.set_nonav(true);
else if (key != "categories") article.emplace(key, value);
else
{
std::istringstream iss(value);
while (std::getline(iss, value, ',')) article.emplace(trim(value));
}
}
}
void create_index(const std::string& name,
const article_list& articles,
const categories_t& categories)
{
using namespace hemplate; // NOLINT
std::ofstream ost(name + ".html");
std::stringstream strs;
strs << html::h1(name);
strs << html::ul().set("class", "index");
for (const auto& article : articles)
{
if (article->is_hidden()) continue;
const auto& title = article->get_title();
const auto& date = article->get_date();
strs << html::li()
.add(html::div(std::format("{} - ", date)))
.add(html::div().add(html::a(title).set("href", title)));
};
strs << html::ul();
article index(name, categories);
index.write(strs.str(), ost);
}
void create_atom(const std::string& name, const article_list& articles)
{
using namespace hemplate; // NOLINT
static const char* base = "https://dimitrijedobrota.com/blog";
static const char* loc = "https://dimitrijedobrota.com/blog/atom.feed";
static const char* updated = "2003-12-13T18:30:02Z";
static const char* summary = "Click on the article link to read...";
std::ofstream ost(name + ".atom");
elementList content = std::accumulate(
begin(articles),
end(articles),
elementList(),
[](elementList&& list, const auto& article)
{
const auto title = article->get_title();
list.add(atom::entry()
.add(atom::title(title))
.add(atom::link().set(
"href", std::format("{}/{}.html", base, title)))
.add(atom::updated(updated))
.add(atom::summary(summary)));
return std::move(list);
});
ost << atom::xml({{"version", "1.0"}, {"encoding", "utf-8"}});
ost << atom::feed();
ost << atom::title(name);
ost << atom::link().set("href", base);
ost << atom::link({{"rel", "self"}, {"href", loc}});
ost << atom::id(base);
ost << atom::updated(updated);
ost << atom::author().add(atom::name(name));
ost << atom::feed();
ost << content;
ost << atom::feed(content);
}
void create_rss(const std::string& name, const article_list& articles)
{
using namespace hemplate; // NOLINT
std::ofstream ost(name + ".rss");
static const char* author = "Dimitrije Dobrota";
static const char* email = "mail@dimitrijedobrota.com";
static const char* base = "https://dimitrijedobrota.com/blog";
static const char* description = "Contents of Dimitrije Dobrota's webpage";
static const char* loc = "https://dimitrijedobrota.com/blog/index.rss";
static const char* updated = "2003-12-13T18:30:02Z";
elementList content = std::accumulate(
begin(articles),
end(articles),
elementList(),
[](elementList&& list, const auto& article)
{
const auto title = article->get_title();
list.add(rss::item()
.add(rss::title(title))
.add(rss::link(std::format("{}/{}.html", base, title)))
.add(rss::guid(std::format("{}/{}.html", base, title)))
.add(rss::pubDate(updated))
.add(rss::author(std::format("{} ({})", email, author))));
return std::move(list);
});
ost << rss::xml({{"version", "1.0"}, {"encoding", "utf-8"}});
ost << rss::rss(
{{"version", "2.0"}, {"xmlns:atom", "http://www.w3.org/2005/Atom"}});
ost << rss::channel();
ost << rss::title(name);
ost << rss::link(base);
ost << rss::description(description);
ost << rss::generator("stamd");
ost << rss::language("en-us");
ost << rss::atomLink(
{{"href", loc}, {"rel", "self"}, {"type", "application/rss+xml"}});
ost << content;
ost << rss::channel();
ost << rss::rss();
}
struct arguments_t
{
std::string output_dir = ".";
std::vector<std::string> articles;
};
int parse_opt(int key, const char* arg, poafloc::Parser* parser)
{
auto* args = static_cast<arguments_t*>(parser->input());
switch (key)
{
case 'o':
args->output_dir = arg;
break;
case poafloc::ARG:
args->articles.emplace_back(arg);
break;
defaut:
poafloc::help(parser, stderr, poafloc::STD_USAGE);
break;
}
return 0;
}
// NOLINTBEGIN
static const poafloc::option_t options[] = {
{"output", 'o', "DIR", 0, "Output directory"},
{0},
};
static const poafloc::arg_t arg {
options,
parse_opt,
"config_file",
"",
};
// NOLINTEND
int main(int argc, char* argv[])
{
using category_map_t = std::unordered_map<std::string, article_list>;
arguments_t args;
if (poafloc::parse(&arg, argc, argv, 0, &args) != 0)
{
std::cerr << "There was an error while parsing arguments";
return 1;
}
category_map_t category_map;
categories_t all_categories;
article_list all_articles;
maddy::Parser parser;
for (const auto& name : args.articles)
{
std::ofstream ofs(name + ".out");
std::ifstream ifs(name);
all_articles.push_back(make_shared<article>(name));
auto& article = all_articles.back();
preprocess(*article, ifs);
article->write(parser.Parse(ifs), ofs);
if (!article->is_hidden())
{
all_categories.merge(article->get_categories());
for (const auto& ctgry : article->get_categories())
category_map[ctgry].push_back(article);
}
}
create_rss("index", all_articles);
create_atom("index", all_articles);
create_index("index", all_articles, all_categories);
for (const auto& [category, articles] : category_map)
{
auto ctgry = category;
create_index(normalize(ctgry), articles, {});
}
return 0;
}

diff --git a/source/utility.hpp b/source/utility.hpp

@@ -0,0 +1,36 @@

#pragma once
#include <algorithm>
#include <string>
inline std::string& ltrim(std::string& str)
{
str.erase(
str.begin(),
std::find_if(str.begin(),
str.end(),
[](unsigned char chr) { return std::isspace(chr) == 0; }));
return str;
}
inline std::string& rtrim(std::string& str)
{
str.erase(
std::find_if(str.rbegin(),
str.rend(),
[](unsigned char chr) { return std::isspace(chr) == 0; })
.base(),
str.end());
return str;
}
inline std::string& trim(std::string& str)
{
return rtrim(ltrim(str));
}
inline std::string& normalize(std::string& str)
{
std::replace(str.begin(), str.end(), ' ', '_');
return str;
}

diff --git a/src/dir.info b/src/dir.info

diff --git a/src/stamd.c b/src/stamd.c

@@ -1,589 +0,0 @@

#define _XOPEN_SOURCE 700
#include <ctype.h>
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "md4c-html.h"
#include <cii/atom.h>
#include <cii/list.h>
#include <cii/mem.h>
#include <cii/str.h>
#include <cii/table.h>
#define MAX_SIZE 100
#define BASE_URL "https://dimitrijedobrota.com/blog"
#define TITLE "Dimitrije Dobrota's blog"
#define AUTHOR "Dimitrije Dobrota"
#define AUTHOR_EMAIL "mail@dimitrijedobrota.com"
#define SETTINGS_TIME_FORMAT "%Y-%m-%d"
#define ATOM_TIME_FORMAT "%Y-%m-%dT%H:%M:%SZ"
#define ATOM_FILE "index.atom"
#define ATOM_LOCATION BASE_URL "/" ATOM_FILE
#define RSS_TIME_FORMAT "%a, %d %b %Y %H:%M:%S +0200"
#define RSS_FILE "rss.xml"
#define RSS_LOCATION BASE_URL "/" RSS_FILE
#define SITEMAP "%a, %d %b %Y %H:%M:%S +0200"
#define SITEMAP_FILE "sitemap.xml"
List_T articles; /* List of all articles */
List_T articlesVisible; /* List of all articles that are not hidden*/
Table_T category_table; /* Table of all non hidden articles for each category*/
void usage(char *argv0) {
fprintf(stderr, "Usage: %s [-o output_dir] article\n", argv0);
exit(EXIT_FAILURE);
}
void process(const MD_CHAR *text, MD_SIZE size, void *userdata) {
fprintf((FILE *)userdata, "%.*s", size, text);
}
char *memory_open(char *infile, int *size) {
char *addr;
int fd, ret;
struct stat st;
if ((fd = open(infile, O_RDWR)) < 0)
error(EXIT_FAILURE, errno, "%s", infile);
if ((ret = fstat(fd, &st)) < 0)
error(EXIT_FAILURE, errno, "line %d, fstat", __LINE__);
if ((addr = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd,
0)) == MAP_FAILED)
error(EXIT_FAILURE, errno, "line %d, addr", __LINE__);
if (size)
*size = st.st_size;
return addr;
}
void memory_close(char *content, int size) {
if (munmap(content, size) == -1)
error(EXIT_FAILURE, errno, "line %d, munmap", __LINE__);
}
char *normalize(char *name) {
char *s = name, *c = name;
while (isspace(*c))
c++;
for (; *c; c++)
if (isspace(*c) && *(name - 1) != '_')
*name++ = '_';
else if (isalnum(*c))
*name++ = *c;
*name = '\0';
return s;
}
void strip_whitspace(char *str) {
char *p;
if (!str)
return;
p = str + strlen(str);
do {
p--;
} while (isspace(*p));
*(p + 1) = '\0';
p = str;
while (isspace(*p))
p++;
while (*p)
*str++ = *p++;
*str = '\0';
}
int strscmp(const void *a, const void *b) {
return -strcmp((char *)b, (char *)a);
}
void applyListFree(void **ptr, void *cl) { FREE(*ptr); }
#define T Article_T
typedef struct T *T;
struct T {
char *content;
char *output_dir;
int content_size;
int hidden;
int nonav;
FILE *outfile;
Table_T symbols;
List_T categories;
};
#define AP(...) fprintf(article->outfile, __VA_ARGS__);
T Article_new(char *output_dir, char *title) {
T p;
NEW0(p);
if (!title)
title = "article";
p->output_dir = output_dir;
p->symbols = Table_new(0, NULL, NULL);
p->categories = List_list(NULL);
Table_put(p->symbols, Atom_string("title"), title);
Table_put(p->symbols, Atom_string("date"), "1970-01-01");
Table_put(p->symbols, Atom_string("lang"), "en");
return p;
}
void Article_setContent(T self, char *content, int content_size) {
self->content = content;
self->content_size = content_size;
}
int Article_cmp(const void *a, const void *b) {
Article_T a1 = (Article_T)a;
Article_T a2 = (Article_T)b;
int res = strcmp(Table_get(a1->symbols, Atom_string("date")),
Table_get(a2->symbols, Atom_string("date")));
if (res)
return -res;
return strcmp(Table_get(a1->symbols, Atom_string("title")),
Table_get(a2->symbols, Atom_string("title")));
}
void Article_openWrite(T self) {
char outfile[2 * PATH_MAX];
if (!Table_get(self->symbols, Atom_string("filename"))) {
Table_put(self->symbols, Atom_string("filename"),
Str_dup(Table_get(self->symbols, Atom_string("title")), 1, 0, 1));
}
char *filename = Table_get(self->symbols, Atom_string("filename"));
normalize(filename);
sprintf(outfile, "%s/%s.html", self->output_dir, filename);
if ((self->outfile = fopen(outfile, "w")) == NULL)
error(EXIT_FAILURE, errno, "line %d, fopen(%s)", __LINE__, outfile);
}
void Article_closeWrite(T self) {
if (self->outfile)
fclose(self->outfile);
}
void Article_free(T self) {
Table_free(&self->symbols);
/* List_map(self->categories, applyListFree, NULL); */
List_free(&self->categories);
FREE(self);
}
void print_category_item(void **item, void *article_pointer) {
char *category = *(char **)item, *norm;
Article_T article = (Article_T)article_pointer;
norm = normalize(Str_dup(category, 1, 0, 1));
AP("<a href=\"./%s.html\">%s</a>\n", norm, category);
FREE(norm);
}
void print_header(T article) {
AP("<!DOCTYPE html>\n"
"<html lang=\"%s\">\n"
"<head>\n"
"<title>%s</title>\n",
(char *)Table_get(article->symbols, Atom_string("lang")),
(char *)Table_get(article->symbols, Atom_string("title")));
AP("<meta charset=\"UTF-8\" />\n"
"<meta name=\"viewport\" "
"content=\"width=device-width,initial-scale=1\"/>\n"
"<meta name=\"description\" content=\"Dimitrije Dobrota's personal site. "
"You can find my daily findings in a form of articles on my blog as well "
"as various programming projects.\" />\n"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/index.css\" />\n"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/css/colors.css\" />\n"
"<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" "
"href=\"/img/favicon-32x32.png\">\n"
"<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" "
"href=\"/img/favicon-16x16.png\">\n"
"</head>\n"
"<body>\n"
"<input type=\"checkbox\" id=\"theme_switch\" class=\"theme_switch\">\n"
"<main>\n"
"<div class=\"content\">\n"
"<label for=\"theme_switch\" class=\"switch_label\"></label>\n");
if (article->nonav)
return;
AP("<div>\n<nav><a class=\"back\">&lt;-- back</a><a "
"href=\"" BASE_URL "\">index</a><a href=\"/\">home "
"--&gt;</a></nav><hr>\n</div>\n");
if (List_length(article->categories) > 0) {
List_sort(article->categories, strscmp);
AP("<div class=\"categories\"><h3>Categories:</h3><p>\n");
List_map(article->categories, print_category_item, article);
AP("</p></div>\n");
}
}
void print_footer(T article) {
if (!article->nonav) {
AP("<div class=\"bottom\">\n<hr>\n<nav>\n<a class=\"back\">&lt;-- "
"back</a><a href=\"" BASE_URL "\">index</a><a href=\"/\">home "
"--&gt;</a></nav></div>\n");
}
AP("</div>\n</main>\n"
"<script src=\"/scripts/main.js\"></script>\n"
"</body>\n</html>\n");
}
void print_index_item(void **article_item_pointer, void *article_pointer) {
Article_T article_item = *(Article_T *)article_item_pointer;
Article_T article = (Article_T)article_pointer;
AP("<li><div>%s - </div><div><a href=\"%s.html\">%s</a></div></li>\n",
(char *)Table_get(article_item->symbols, Atom_string("date")),
(char *)Table_get(article_item->symbols, Atom_string("filename")),
(char *)Table_get(article_item->symbols, Atom_string("title")))
}
void print_index(Article_T article, List_T articles, List_T categories) {
article->categories = categories;
Article_openWrite(article);
print_header(article);
{
List_sort(articles, Article_cmp);
AP("<h1>%s</h1>\n",
(char *)Table_get(article->symbols, Atom_string("title")));
AP("<ul class=\"index\">\n");
List_map(articles, print_index_item, article);
AP("</ul>\n");
}
print_footer(article);
Article_closeWrite(article);
Article_free(article);
}
void Article_preprocess(T self) {
char *text = self->content;
char *line;
for (line = strtok(text, "\n"); line; line = strtok(NULL, "\n")) {
strip_whitspace(line);
if (!*line)
continue;
else if (*line == '@') {
char *keys, *values;
keys = CALLOC(1000, sizeof(char));
values = CALLOC(1000, sizeof(char));
sscanf(line, " @%[^:]: %[^\n] ", keys, values);
if (!strcmp(keys, "hidden"))
self->hidden = 1;
else if (!strcmp(keys, "nonav"))
self->nonav = 1;
else
Table_put(self->symbols, Atom_string(keys), Str_dup(values, 1, 0, 1));
FREE(values);
FREE(keys);
} else {
*(line + strlen(line)) = '\n';
text = line;
break;
}
}
self->content_size = self->content_size - (text - self->content);
self->content = text;
char *cat;
if ((cat = (char *)Table_get(self->symbols, Atom_string("categories")))) {
char delim[] = ",", *category;
for (category = strtok(cat, delim); category;
category = strtok(NULL, delim)) {
if (strlen(category) > 1) {
strip_whitspace(category);
const char *atom = Atom_string(category);
self->categories = List_push(self->categories, category);
/* append the article to the list of articles for a current category*/
if (!self->hidden)
Table_put(category_table, atom,
List_push(Table_get(category_table, atom), self));
}
}
}
if (!self->hidden)
articlesVisible = List_push(articlesVisible, self);
}
void Article_translate(T self) {
Article_preprocess(self);
Article_openWrite(self);
print_header(self);
md_html(self->content, self->content_size, process, self->outfile,
MD_DIALECT_GITHUB, 0);
print_footer(self);
Article_closeWrite(self);
}
char *get_date(char *date_str, char **date_buf, char *conversion) {
struct tm date;
memset(&date, 0, sizeof(date));
strptime(date_str, SETTINGS_TIME_FORMAT, &date);
strftime(*date_buf, MAX_SIZE, conversion, &date);
return *date_buf;
}
void print_atom_item(void **article_item_pointer, void *file_pointer) {
Article_T article_item = *(Article_T *)article_item_pointer;
char *date_buffer = ALLOC(MAX_SIZE);
FILE *f = (FILE *)file_pointer;
get_date((char *)Table_get(article_item->symbols, Atom_string("date")),
&date_buffer, ATOM_TIME_FORMAT);
fprintf(f,
"<entry>\n"
" <title>%s</title>\n"
" <link href=\"" BASE_URL "/%s.html\"/>\n"
" <id>" BASE_URL "/%s.html</id>\n"
" <updated>%s</updated>\n"
" <summary>Click on the article link to read...</summary>\n"
"</entry>\n",
(char *)Table_get(article_item->symbols, Atom_string("title")),
(char *)Table_get(article_item->symbols, Atom_string("filename")),
(char *)Table_get(article_item->symbols, Atom_string("filename")),
date_buffer);
FREE(date_buffer);
}
void print_atom(List_T articles, FILE *f) {
fprintf(f, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
"<feed xmlns=\"http://www.w3.org/2005/Atom\">\n"
"<title>" TITLE "</title>\n"
"<link href=\"" BASE_URL "/\"/>\n"
"<link rel=\"self\" "
"href=\"" ATOM_LOCATION "\" />\n"
"<id>" BASE_URL "</id>\n"
"<updated>2003-12-13T18:30:02Z</updated>\n"
"<author>\n"
"<name>" AUTHOR "</name>\n"
"</author>\n");
List_map(articles, print_atom_item, f);
fprintf(f, "</feed>\n");
}
void print_rss_item(void **article_item_pointer, void *file_pointer) {
Article_T article_item = *(Article_T *)article_item_pointer;
char *date_buffer = ALLOC(MAX_SIZE);
FILE *f = (FILE *)file_pointer;
get_date((char *)Table_get(article_item->symbols, Atom_string("date")),
&date_buffer, RSS_TIME_FORMAT);
fprintf(f,
"<item>\n"
" <title>%s</title>\n"
" <link>" BASE_URL "/%s.html</link>\n"
" <guid>" BASE_URL "/%s.html</guid>\n"
" <pubDate>%s</pubDate>\n"
" <author>" AUTHOR_EMAIL " (" AUTHOR ")</author>\n"
"</item>\n",
(char *)Table_get(article_item->symbols, Atom_string("title")),
(char *)Table_get(article_item->symbols, Atom_string("filename")),
(char *)Table_get(article_item->symbols, Atom_string("filename")),
date_buffer);
FREE(date_buffer);
}
void print_rss(List_T articles, FILE *f) {
fprintf(f,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
"<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n"
"<channel>\n"
"<title>" TITLE "</title>\n"
"<link>" BASE_URL "</link>\n"
"<description>Contents of Dimitrije Dobrota's webpage</description>"
"<generator>stamd</generator>"
"<language>en-us</language>\n"
"<atom:link href=\"" RSS_LOCATION "\" rel=\"self\" "
"type=\"application/rss+xml\" />");
List_map(articles, print_rss_item, f);
fprintf(f, "</channel>\n"
"</rss>\n");
}
void print_sitemap_item(void **article_item_pointer, void *file_pointer) {
Article_T article_item = *(Article_T *)article_item_pointer;
char *date_buffer = ALLOC(MAX_SIZE);
FILE *f = (FILE *)file_pointer;
get_date((char *)Table_get(article_item->symbols, Atom_string("date")),
&date_buffer, RSS_TIME_FORMAT);
fprintf(f,
"<url>\n"
" <loc>" BASE_URL "/%s.html</loc>\n"
" <changefreq>weekly</changefreq>\n"
"</url>\n",
(char *)Table_get(article_item->symbols, Atom_string("filename")));
FREE(date_buffer);
}
void print_sitemap(List_T articles, FILE *f) {
fprintf(f,
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
"<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
List_map(articles, print_sitemap_item, f);
fprintf(f, "</ulrset>\n");
}
/* void print_xml(char *file_name, void(*)) */
int main(int argc, char *argv[]) {
char output_dir[PATH_MAX];
int opt;
while ((opt = getopt(argc, argv, "o:")) != -1) {
switch (opt) {
case 'o':
if (!realpath(optarg, output_dir))
error(EXIT_FAILURE, errno, "-o %s", optarg);
break;
default:
usage(argv[0]);
}
}
if (optind >= argc)
usage(argv[0]);
if (!*output_dir)
realpath(".", output_dir);
category_table = Table_new(0, NULL, NULL);
articles = List_list(NULL);
articlesVisible = List_list(NULL);
for (; optind < argc; optind++) {
T article;
char *content;
int content_size;
content = memory_open(argv[optind], &content_size);
article = Article_new(output_dir, NULL);
Article_setContent(article, content, content_size);
Article_translate(article);
memory_close(content, content_size);
articles = List_push(articles, article);
}
/* Print main index and index for each encountered category*/
{
List_T categories = List_list(NULL);
void **array = Table_toArray(category_table, NULL);
for (int i = 0; array[i]; i += 2) {
categories = List_push(categories, array[i]);
print_index(Article_new(output_dir, array[i]), array[i + 1], NULL);
}
if (List_length(articlesVisible) > 1) {
print_index(Article_new(output_dir, "index"), articlesVisible,
categories);
char outfile[2 * PATH_MAX];
FILE *f;
sprintf(outfile, "%s/%s", output_dir, ATOM_FILE);
f = fopen(outfile, "w");
print_atom(articlesVisible, f);
fclose(f);
sprintf(outfile, "%s/%s", output_dir, RSS_FILE);
f = fopen(outfile, "w");
print_rss(articlesVisible, f);
fclose(f);
sprintf(outfile, "%s/%s", output_dir, SITEMAP_FILE);
f = fopen(outfile, "w");
print_sitemap(articlesVisible, f);
fclose(f);
}
FREE(array);
}
/* Free category table*/
{
List_T *symbols = (List_T *)Table_toArray(category_table, NULL);
for (int i = 0; symbols[i]; i += 2)
List_free(&symbols[i + 1]);
FREE(symbols);
}
/* Free articles */
{
Article_T *article_list = (Article_T *)List_toArray(articles, NULL);
for (int i = 0; article_list[i]; i++)
Article_free(article_list[i]);
FREE(article_list);
}
Table_free(&category_table);
List_free(&articles);
List_free(&articlesVisible);
return 0;
}

diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt

@@ -0,0 +1,17 @@

# Parent project does not export its library target, so this CML implicitly
# depends on being added from it, i.e. the testing is done only from the build
# tree and is not feasible from an install location
project(stamdTests LANGUAGES CXX)
# ---- Tests ----
add_executable(stamd_test source/stamd_test.cpp)
target_link_libraries(stamd_test PRIVATE stamd_lib)
target_compile_features(stamd_test PRIVATE cxx_std_20)
add_test(NAME stamd_test COMMAND stamd_test)
# ---- End-of-file commands ----
add_folders(Test)

diff --git a/test/source/stamd_test.cpp b/test/source/stamd_test.cpp

@@ -0,0 +1,4 @@

int main()
{
return 0;
}