commit 99bcf0d96824df5e573ba76a2ac7c261133c596d
parent 04e983cf30b9c6a9a45da5980e2354ade6ef6654
Author: Dimitrije Dobrota <mail@dimitrijedobrota.com>
Date: Mon, 24 Jun 2024 22: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:
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,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("<-- back", {{"class", "back"}}))
+ .add(html::a("index", {{"href", base}}))
+ .add(html::a("hime -->", {{"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\"><-- back</a><a "
- "href=\"" BASE_URL "\">index</a><a href=\"/\">home "
- "--></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\"><-- "
- "back</a><a href=\"" BASE_URL "\">index</a><a href=\"/\">home "
- "--></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;
+}