stamd

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

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:
M.clang-format | 218++++++++++++++++++++++++++++++++++++++++----------------------------------------
A.clang-tidy | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.codespellrc | 6++++++
A.github/workflows/ci.yml | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M.gitignore | 69+++++++++++----------------------------------------------------------
ABUILDING.md | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACMakeLists.txt | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACMakePresets.json | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACODE_OF_CONDUCT.md | 5+++++
ACONTRIBUTING.md | 14++++++++++++++
AHACKING.md | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE.md | 21+++++++++++++++++++++
DMakefile | 51---------------------------------------------------
MREADME.md | 16+++++++++++++++-
Dbin/dir.info | 0
Acmake/coverage.cmake | 33+++++++++++++++++++++++++++++++++
Acmake/dev-mode.cmake | 23+++++++++++++++++++++++
Acmake/folders.cmake | 21+++++++++++++++++++++
Acmake/install-rules.cmake | 8++++++++
Acmake/lint-targets.cmake | 33+++++++++++++++++++++++++++++++++
Acmake/lint.cmake | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Acmake/prelude.cmake | 10++++++++++
Acmake/project-is-top-level.cmake | 6++++++
Acmake/spell-targets.cmake | 22++++++++++++++++++++++
Acmake/spell.cmake | 29+++++++++++++++++++++++++++++
Acmake/variables.cmake | 28++++++++++++++++++++++++++++
Dinclude/dir.info | 0
Dobj/dir.info | 0
Asource/article.cpp | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/article.hpp | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/main.cpp | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/utility.hpp | 36++++++++++++++++++++++++++++++++++++
Dsrc/dir.info | 0
Dsrc/stamd.c | 589-------------------------------------------------------------------------------
Atest/CMakeLists.txt | 17+++++++++++++++++
Atest/source/stamd_test.cpp | 4++++
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("&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; +}