├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ ├── README.md │ ├── analysis.yml │ ├── docker.ci.yml │ ├── docker.release.yml │ └── nodejs.ci.yml ├── .gitignore ├── .npmignore ├── CONTRIBUTE.md ├── Dockerfile ├── HISTORY.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── app ├── core │ ├── contents.js │ ├── language.js │ ├── lunr.js │ ├── page.js │ ├── search.js │ └── utils.js ├── functions │ ├── build_nested_pages.js │ ├── content_processors.js │ ├── create_meta_info.js │ ├── get_filepath.js │ ├── remove_image_content_directory.js │ ├── sanitize.js │ └── sanitize_markdown.js ├── index.js ├── middleware │ ├── always_authenticate.mw.js │ ├── authenticate.mw.js │ ├── authenticate_read_access.mw.js │ ├── error_handler.mw.js │ └── oauth2.mw.js ├── routes │ ├── category.create.route.js │ ├── home.route.js │ ├── login.route.js │ ├── login_page.route.js │ ├── logout.route.js │ ├── page.create.route.js │ ├── page.delete.route.js │ ├── page.edit.route.js │ ├── search.route.js │ ├── sitemap.route.js │ └── wildcard.route.js └── translations │ ├── da.json │ ├── de.json │ ├── en.json │ ├── es.json │ ├── fa.json │ ├── fi.json │ ├── fr.json │ ├── hu.json │ ├── ja.json │ ├── no.json │ ├── pl.json │ ├── ptbr.json │ ├── ro.json │ ├── ru.json │ ├── sv.json │ ├── tr.json │ └── zh.json ├── config └── config.js ├── content ├── pages │ ├── community │ │ ├── contributing.md │ │ ├── credits.md │ │ ├── related-projects.md │ │ ├── roadmap.md │ │ ├── showcase.md │ │ └── sort │ ├── deployment │ │ ├── containers.md │ │ ├── production-notes.md │ │ ├── security.md │ │ ├── sort │ │ └── updating-raneto.md │ ├── getting-started.md │ ├── install │ │ ├── installing-raneto.md │ │ ├── requirements.md │ │ └── sort │ ├── news.md │ ├── tutorials │ │ ├── customizing-the-template.md │ │ ├── deploying-raneto-to-heroku.md │ │ ├── google-oauth-setup.md │ │ ├── running-as-a-service.md │ │ └── sort │ ├── usage │ │ ├── authentication.md │ │ ├── category-meta.md │ │ ├── configuration.md │ │ ├── creating-pages.md │ │ ├── custom-homepage.md │ │ ├── deleting-pages.md │ │ ├── highlight-code.md │ │ ├── meta │ │ ├── page-meta.md │ │ └── sorting.md │ └── what-is-raneto.md └── static │ └── readme.txt ├── deprecated ├── bin │ └── raneto └── multiple-instances.js ├── logo ├── Raneto_Logo.psd ├── logo.png └── logo_readme.png ├── package-lock.json ├── package.json ├── server.js └── test ├── content ├── example-page.md ├── example-russian.md ├── hidden-page.md ├── page-with-bom-yaml.md ├── page-with-bom.md ├── special-chars.md └── sub │ ├── hidden │ ├── ignore │ └── not_on_menu.md │ ├── not_on_home │ └── meta │ ├── page.md │ └── sub2 │ └── sub2_page.md ├── functions.test.js └── raneto-core.test.js /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .github/ 3 | deprecated/ 4 | logo/ 5 | node_modules/ 6 | test/ 7 | .dockerignore 8 | .editorconfig 9 | .eslintrc 10 | .gitignore 11 | Dockerfile 12 | Makefile 13 | package-lock.json 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [Makefile] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": false, 5 | "node": true, 6 | "es6": true, 7 | "jest": true 8 | }, 9 | "extends": ["eslint:recommended"], 10 | "parserOptions": { 11 | "ecmaVersion": "2022", 12 | "sourceType": "module" 13 | }, 14 | "plugins": ["promise", "unused-imports", "jest"], 15 | "rules": { 16 | "camelcase": 0, 17 | "curly": 2, 18 | "eqeqeq": 2, 19 | "func-call-spacing": 0, 20 | "guard-for-in": 2, 21 | "indent": ["error", 2, { "SwitchCase": 1 }], 22 | "key-spacing": 0, 23 | "max-depth": ["error",{"max" : 4}], 24 | "no-irregular-whitespace": 2, 25 | "no-multi-spaces": 0, 26 | "padded-blocks": 0, 27 | "quotes": ["error", "single", { "allowTemplateLiterals": true }], 28 | "semi": 0, 29 | "no-path-concat": 1, 30 | "no-undef": 2, 31 | "unused-imports/no-unused-imports": "error", 32 | "no-unused-vars": 2, 33 | "no-var": 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # GitHub Actions 2 | 3 | Documentation and search here 4 | https://github.com/marketplace?type=actions 5 | -------------------------------------------------------------------------------- /.github/workflows/analysis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 3 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 4 | 5 | name: Code Analysis 6 | on: 7 | push: 8 | branches: ['main'] 9 | pull_request: 10 | branches: ['main'] 11 | 12 | jobs: 13 | build: 14 | environment: build 15 | # OS List 16 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories 17 | runs-on: ubuntu-latest 18 | steps: 19 | # https://github.com/marketplace/actions/checkout 20 | - uses: actions/checkout@v4.2.2 21 | # https://github.com/marketplace/actions/setup-node-js-environment 22 | - name: Use Node.js 22.x 23 | uses: actions/setup-node@v4.2.0 24 | with: 25 | node-version: 22.x 26 | #cache: 'npm' 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm run unit 30 | # https://github.com/marketplace/actions/codecov 31 | - name: Upload coverage reports to Codecov 32 | uses: codecov/codecov-action@v5.3.1 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | # https://github.com/marketplace/actions/official-fossa-action 36 | - name: Upload to FOSSA 37 | uses: fossas/fossa-action@v1.5.0 38 | with: 39 | api-key: ${{ secrets.FOSSA_API_KEY }} 40 | -------------------------------------------------------------------------------- /.github/workflows/docker.ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://docs.github.com/en/actions/publishing-packages/publishing-docker-images 3 | 4 | name: Docker CI 5 | on: 6 | push: 7 | branches: ['main'] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | environment: Docker 13 | 14 | steps: 15 | # https://github.com/marketplace/actions/checkout 16 | # - name: Build the Docker image 17 | # uses: actions/checkout@v4.2.2 18 | # run: docker build . --file Dockerfile --tag raneto/raneto:latest-$(date +%s) 19 | 20 | # https://github.com/marketplace/actions/docker-setup-buildx 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3.8.0 23 | 24 | # https://github.com/marketplace/actions/docker-login 25 | # https://github.com/docker/login-action 26 | - name: Login to Docker Hub 27 | uses: docker/login-action@v3.3.0 28 | with: 29 | username: ${{ secrets.DOCKER_USERNAME }} 30 | password: ${{ secrets.DOCKER_TOKEN }} 31 | 32 | # https://github.com/marketplace/actions/build-and-push-docker-images 33 | - name: Build and push 34 | uses: docker/build-push-action@v6.13.0 35 | with: 36 | #context: . 37 | push: true 38 | tags: raneto/raneto:unstable 39 | -------------------------------------------------------------------------------- /.github/workflows/docker.release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/ 3 | 4 | name: Docker Release 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'Release Version' 10 | required: true 11 | default: 'latest' 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | environment: Docker 17 | 18 | steps: 19 | # https://github.com/marketplace/actions/checkout 20 | # - name: Build the Docker image 21 | # uses: actions/checkout@v4.2.2 22 | # run: docker build . --file Dockerfile --tag raneto/raneto:latest-$(date +%s) 23 | 24 | # https://github.com/marketplace/actions/docker-setup-buildx 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3.8.0 27 | 28 | # https://github.com/marketplace/actions/docker-login 29 | # https://github.com/docker/login-action 30 | - name: Login to Docker Hub 31 | uses: docker/login-action@v3.3.0 32 | with: 33 | username: ${{ secrets.DOCKER_USERNAME }} 34 | password: ${{ secrets.DOCKER_TOKEN }} 35 | 36 | # https://github.com/marketplace/actions/build-and-push-docker-images 37 | # https://docs.github.com/en/actions/publishing-packages/publishing-docker-images 38 | - name: Build and push "version" 39 | uses: docker/build-push-action@v6.13.0 40 | with: 41 | #context: . 42 | push: true 43 | tags: raneto/raneto:${{ github.event.inputs.version }},raneto/raneto:latest 44 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 3 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 4 | 5 | name: Node.js CI 6 | on: 7 | push: 8 | branches: ['main'] 9 | pull_request: 10 | branches: ['main'] 11 | 12 | jobs: 13 | build: 14 | environment: build 15 | strategy: 16 | matrix: 17 | # Windows is currently borked 18 | # https://github.com/nodejs/node/issues/52682 19 | # Disabling for now 20 | # windows-latest 21 | os: [ubuntu-latest] 22 | node_version: [22.x, 23.x] 23 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 24 | 25 | # OS List 26 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories 27 | runs-on: ${{ matrix.os }} 28 | steps: 29 | # https://github.com/marketplace/actions/checkout 30 | - uses: actions/checkout@v4.2.2 31 | # https://github.com/marketplace/actions/setup-node-js-environment 32 | - name: Use Node.js ${{ matrix.node_version }} 33 | uses: actions/setup-node@v4.2.0 34 | with: 35 | node-version: ${{ matrix.node_version }} 36 | #cache: 'npm' 37 | - run: npm ci 38 | - run: npm run build --if-present 39 | - run: npm test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (https://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | node_modules 24 | 25 | # npm Components (Installed Automatically) 26 | themes/default/public/lib/ 27 | 28 | # Misc 29 | *.pages 30 | *.pdf 31 | .DS_Store 32 | 33 | .idea 34 | 35 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .editorconfig 3 | .eslintrc.json 4 | .gitignore 5 | .github 6 | deprecated 7 | Dockerfile 8 | logo 9 | Makefile 10 | test 11 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | We do our best to accept all PRs that help advance the project. 5 | Thanks for your contributions! 6 | 7 | ## Contribution License Agreement 8 | 9 | If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. 10 | 11 | ## Process 12 | 13 | 1. Fork the Raneto repository 14 | 1. Make your edits 15 | 1. Open a Pull Request against the `main` branch ([Here's How](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github#create-pull-request)) 16 | 1. Ensure all tests pass in the PR (and hopefully you added a few tests too?) 17 | 1. Wait for review and discussion. It's ok to bump your request after a few days to remind us if you haven't gotten a response. 18 | 1. Your code will be merged if accepted! 19 | 20 | ## Reasons for PR Rejection 21 | 22 | 1. Tests are failing according to CI Pipeline on supported Node.js versions 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22.13.1-alpine3.21 2 | 3 | EXPOSE 3000 4 | ENV HOST 0.0.0.0 5 | ENV PORT 3000 6 | 7 | WORKDIR /opt/raneto 8 | COPY . /opt/raneto 9 | 10 | RUN npm install --omit=dev 11 | 12 | CMD ["npm", "start"] 13 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Raneto Changelog 2 | 3 | ## RELEASE TBD / v0.18.0 4 | 5 | This is a modernization refactor. 6 | 7 | - **[BREAKING]** Minimum Node.js is v22.x LTS 8 | - **[BREAKING]** Rename branch to "main" from "master" 9 | - **[BREAKING]** Packages removed: `pm2`, `commander`, `tail`, `serve-favicon` 10 | - **[BREAKING]** `bin/raneto` removed as it is out of scope. Please do not use PM2 and instead use `npm start`, containers, systemd, etc. 11 | - **[BREAKING]** Modernizing, moving to ESModules, `require => import`, `const/let`, `node:` import prefix, `module.exports => export`, `'use strict';` etc. 12 | - **[Misc]** Dependency upgrades 13 | - **[Fix]** Windows support (mainline versions only) 14 | - STRETCH CI GitHub Actions tests on branches/forks 15 | - STRETCH CI GitHub Actions container build 16 | 17 | ## 2024.02.22 / v0.17.8 18 | 19 | - **[New]** ShowOnMenu by @mgdesign #388 20 | - **[Misc]** Upgrading to latest Node.js LTS v18.x and v20.x 21 | - **[Misc]** Linting / Prettier 22 | 23 | ## 2024.02.21 / v0.17.7 24 | 25 | - **[Fix]** Markdown content parser/sanitization (https://github.com/ryanlelek/Raneto/commit/863aaf5095010e1013715e16e4fd474166c2591a) 26 | - **[Misc]** Dependency upgrades (https://github.com/ryanlelek/Raneto/commit/ed4f09780539644ac82b7767f911a061c7395d40) 27 | 28 | ## 2023.11.05 / v0.17.6 29 | 30 | - **[Misc]** Dependency upgrades 31 | 32 | ## 2023.06.20 / v0.17.5 33 | 34 | - **[Misc]** Dependency upgrades 35 | 36 | ## 2023.04.11 / v0.17.3 37 | 38 | - **[New]** Linter Updates 39 | -- https://github.com/ryanlelek/Raneto/commit/dddade7e5b8f49f8ec9171265d2201bfad11fa50 (ESLint 1) 40 | -- https://github.com/ryanlelek/Raneto/commit/563756816957f7389fe7d615604f2f486f9155e5 (ESLint 2) 41 | -- https://github.com/ryanlelek/Raneto/commit/878e95b3a0a7398d9e7baf708398745ffcca44a8 (Prettier) 42 | - **[Fix]** Commander/PM2 (Unsupported) 43 | -- https://github.com/ryanlelek/Raneto/commit/19605c228353d2971bd6fbb126a24b696f085851 44 | - **[Misc]** Dependency upgrades 45 | -- Packages current as of today (exception: `glob`) 46 | -- https://github.com/ryanlelek/Raneto/commit/66e08c615eec2fa141ea5512f94f21167e00036d (Container Image) 47 | -- https://github.com/ryanlelek/Raneto/commit/bf6d33ec9f2c9ac31c144155b00091d56e020f6a (NPM Packages) 48 | - Extract `themes/` to new repository/package [@raneto/theme-default](https://github.com/raneto/theme-default) 49 | - Extract `example/` to new [repository](https://github.com/raneto/example) 50 | - **[Removed]** Package `gulp-shell` 51 | - **[Removed]** Package `markdown-it` 52 | 53 | ## 2022.08.28 / v0.17.2 54 | 55 | - **[Fix]** Crash for ignored directories by @pmoleri #369 56 | - **[Docs]** Updated, first pass 57 | - **[Misc]** Pipelines fixed after Travis CI shutdown 58 | - **[Misc]** Dependency upgrades 59 | -- Packages current as of today (exception: bootstrap) 60 | 61 | ## 2022.08.02 / v0.17.1 62 | 63 | This release includes **IMPORTANT - SECURITY FIXES** 64 | 65 | - **[SECURITY]** Sanitization, DoS, Best Practices by @J-GainSec #368 66 | -- Mitigation @ryanlelek #370 67 | - **[New]** Finnish Translation by @Mixerboy24 #363 Oksanen / LocalghostFI Ltd 68 | - **[Fix]** Redirect Fix on server restart, suggested by @leofranke95 #340 69 | - **[Fix]** Docker image build process by @jj-style #355 70 | - **[Fix]** Top bar navigation by @norogoth #357 #358 71 | - **[Fix]** Add Page to Current Category @Meiwer #364 72 | 73 | ## 2021.03.28 / v0.17.0 74 | 75 | Possible breaking changes, based on your implementation 76 | 77 | - **[Edit]** Listening on `127.0.0.1` instead of all interfaces #345 78 | 79 | ## 2021.02.04 / v0.16.6 80 | 81 | - **[Fix]** Example configuration file #338 82 | - contributed by **@ryanlelek** 83 | 84 | ## 2020.12.25 / v0.16.5 85 | 86 | - **[New]** Swedish translation 87 | - contributed by **@Synt3x** 88 | - **[New]** Japanese translation 89 | - contributed by **@filunK** 90 | - **[New]** Add table of contents 91 | - contributed by **@benruehl** 92 | - **[New]** Added side menu collapsing functionality 93 | - contributed by **@philipstratford** 94 | - **[New]** Visibility of menu on pages toggle 95 | - contributed by **@philipstratford** 96 | - **[New]** Google groups restriction 97 | - contributed by **@Axadiw** 98 | - **[New]** Category meta description 99 | 100 | - contributed by **@marcello.gorla** 101 | 102 | - **[Doc]** TOC and site menu on pages 103 | - contributed by **@philipstratford** 104 | - **[Doc]** Updated install, guide, and README pages 105 | 106 | - contributed by **@Arthur Flageul** 107 | 108 | - **[Fix]** Fixed bug highlighting of second-level page titles 109 | - contributed by **@philipstratford** 110 | - **[Fix]** #189 base_url config 111 | - contributed by **@ryanlelek** 112 | - **[Fix]** Side menu visibility 113 | - contributed by **@Synt3x** 114 | - **[Fix]** lunr-languages/tinyseg instead of tiny-segmenter 115 | - contributed by **@filunK** 116 | - **[Fix]** Travis, Yarn, NPM, etc. 117 | - contributed by **@filunK** 118 | - **[Fix]** wrong fitvids js location 119 | - contributed by **@jrichardsz** 120 | 121 | ## 2019.08.11 / v0.16.4 122 | 123 | - **[New]** Async IO Improvements #294 124 | - contributed by **@pmoleri** 125 | - **[New]** Danish Translation #292 126 | - contributed by **@MortenHC** 127 | - **[Fixed]** Heroku postinstall script #291 128 | - contributed by **@shamork** 129 | - **[Fixed]** Code fixes for upgraded dependencies 130 | - contributed by **@ryanlelek** 131 | - **[Misc]** Dependency upgrades 132 | 133 | ## 2019.01.19 / v0.16.2 134 | 135 | - **[New]** Polish Translation 136 | - contributed by **@suprovsky - Radosław Serba** 137 | - **[Fixed]** base_url ignored on login page #200 138 | - contributed by **@GrahamDumpleton** 139 | - **[Fixed]** Request for translations.json doesn't include base_url #279 140 | - contributed by **@GrahamDumpleton** 141 | - **[Fixed]** Proxy subfolders #189 142 | - contributed by **@GrahamDumpleton** 143 | - **[Misc]** Dependency upgrades 144 | 145 | ## 2018.04.21 / v0.16.0 146 | 147 | - **[New]** Better Multi-Language Support! 148 | - contributed by **@Orhideous** 149 | - **[New]** Raneto can be served from non-root path (URI Prefix) 150 | - contributed by **@gugu** 151 | - **[Misc]** Upgrade to lunr v2.x 152 | - contributed by **@Orhideous** 153 | - **[Misc]** Code Refactor 154 | - contributed by **@Orhideous** 155 | - **[Misc]** Dependency upgrades 156 | 157 | ## 2018.03.29 / v0.15.0 158 | 159 | - **[New]** Language Translations! 160 | - Romanian contributed by **@mariuspana** 161 | - **[Fixed]** #192 Any metadata will now cause metadata to render 162 | - **@mralexgray** 163 | - **[Fixed]** Login page loading of jQuery Backstretch plugin 164 | - **@Zezzty** 165 | - **[Fixed]** #247 Search result page no longer shows excerpt as link text 166 | - **@Zezzty** 167 | - **[Fixed]** #251 #194 Documentation in README for local install 168 | - **@shui** 169 | - **[Misc]** Dependency upgrades 170 | 171 | ## 2018.01.09 / v0.14.0 172 | 173 | - **[New]** Language Translations! 174 | - Spanish contributed by **@dgarcia202** 175 | - Norwegian contributed by **@kek91** 176 | - Hungarian contributed by **@gabord** 177 | - **[New]** Multi-level Page Nesting 178 | - **@denisvmedia** 179 | - **[New]** Marking Active Category in UI 180 | - **@pmoleri** 181 | - **[New]** Export of Raneto class 182 | - **@pmoleri** 183 | - **[Improvement]** Search with Special Characters 184 | - **@cassiobsilva** 185 | - **[Improvement]** Upgrade to SweetAlert2 186 | - **@limonte** 187 | - **[Misc]** Remove Babel 188 | - **@pmoleri** 189 | - **[Misc]** Move from JSHint to ESLint 190 | - **@Sparticuz** 191 | - **[Misc]** Code Refinements 192 | - **@furier** 193 | - **@dettoni** 194 | - **@denisvmedia** 195 | - **@dgarcia202** 196 | - **[Misc]** Document Refinements 197 | - **@dgarcia202** 198 | - **@n7st** 199 | - **[Misc]** Dependency upgrades 200 | 201 | ## 2017.03.15 / v0.13.0 202 | 203 | - **[New]** Nested Pages 204 | - contributed by **@zmateusz** 205 | - **[New]** Manual Category Title 206 | - contributed by **@theRealWardo** 207 | - **[New]** Last Edited Metadata Header 208 | - contributed by **@Sparticuz** 209 | - **[New]** Require Authentication for Viewing 210 | - contributed by **@bschne** and **@mohammadrafigh** 211 | - **[Improvement]** Meta Data RegEx Refinement 212 | - contributed by **@cmeyer90** 213 | - **[Improvement]** Unix Sitemap Generation 214 | - contributed by **@forsureitsme** 215 | - **[Improvement]** Display All Files Fix 216 | - contributed by **@forsureitsme** 217 | - **[Misc]** Code Refinements 218 | - **@shyim** 219 | - **@Sparticuz** 220 | - **@theRealWardo** 221 | - **[Misc]** Dependency upgrades 222 | 223 | ## 2016.09.13 / v0.11.0 224 | 225 | - **[New]** Language Translations! 226 | - Mandarin Chinese contributed by **@noahvans** 227 | - French contributed by **@sfoubert** 228 | - Brazilian Portuguese contributed by **@ToasterBR** 229 | - **[New]** Google OAuth Support 230 | - contributed by **@Hitman666** 231 | - **[New]** Authentication for Edit (Public Read-Only) 232 | - contributed by **@alexspeller** 233 | - **[New]** Dynamic Sitemap.xml 234 | - contributed by **@sfoubert** 235 | - **[New]** Custom Variables 236 | - contributed by **@Sparticuz** 237 | - **[Improvement]** Multiple User Login 238 | - contributed by **@mohammadrafigh** 239 | - **[Improvement]** Table of Contents (Dynamic) 240 | - contributed by **@Sparticuz** 241 | - **[Misc]** Merged `Raneto-Core` module into repository 242 | - **[Misc]** Dependency upgrades 243 | 244 | ## 2016.06.18 / v0.10.1 245 | 246 | - **[New]** Language Translations! 247 | - Right to Left support contributed by **@mohammadrafigh** 248 | - Persian contributed by **@mohammadrafigh** 249 | - **[New]** Docker support 250 | - contributed by **@prologic** 251 | - **[Improvement]** Better small-screen layout that automatically hides the left menu 252 | - contributed by **@ezaze** 253 | - **[Misc]** Upgrading raneto-core from v0.4.0 to v0.5.0 254 | 255 | ## 2016.05.22 / v0.10.0 256 | 257 | - **[New]** Raneto Logo 258 | - contributed by **@mmamrila** 259 | - **[New]** Language Translations! 260 | - Russian contributed by **@iam-medvedev** 261 | - Turkish contributed by **@bleda** 262 | - **[New]** Metadata is editable 263 | - contributed by **@draptik** 264 | - \*\*[Fixed] General BugFixes contributed by 265 | - **@draptik** 266 | - **@rogerhutchings** 267 | - **@dncrews** 268 | - **@durand** 269 | 270 | ## 2016.02.13 / v0.9.0 271 | 272 | - **[Fixed]** Embedding images in content 273 | - contributed by **@helenamunoz** 274 | - **[Fixed]** Custom homepage via index.md file 275 | - contributed by **@dirivero** 276 | - **[Fixed]** Sanitizing file paths 277 | - **[New]** German Translation / Locale 278 | - contributed by **@Radiergummi** 279 | - **[New]** Authentication on Changes Only 280 | - contributed by **@Radiergummi** 281 | - **[New]** Vagrant Container 282 | - contributed by **@draptik** 283 | - **[New]** Category in Search Results 284 | - **[New]** Metadata on homepage 285 | - **[Upgraded]** Module raneto-core from v0.2.0 to v0.4.0 286 | - **[Upgraded]** Other Dependencies 287 | - **[Misc]** Broke up code into multiple files 288 | - **[Misc]** Delinted Code 289 | - **[Misc]** Overall refactor 290 | 291 | ## 2015.12.29 / v0.8.0 292 | 293 | - **[Fixed]** URI Decoding with non-Latin characters 294 | - contributed by **@yaruson** 295 | - **[Fixed]** Windows compatability (use `npm run start_win`) 296 | - **[New]** Added Login Page to replace HTTP Basic Auth 297 | - contributed by **@matthiassb** 298 | - **[New]** Added ability to run Raneto as a PM2 service 299 | - contributed by **@matthiassb** 300 | - **[New]** Main Articles is now a category editable in the UI 301 | - contributed by **@yaruson** 302 | - **[New]** Using NPM for client-side libraries 303 | - contributed by **@sbussard** 304 | - **[Upgraded]** Improved Live Editor layout 305 | - contributed by **@draptik** 306 | - **[Removed]** Bower for client-side libraries 307 | - contributed by **@sbussard** 308 | 309 | ## 2015.10.11 / v0.7.1 310 | 311 | - **[New]** Theme support. Copy `themes/default/` to `themes//` and edit. 312 | - **[New]** Added toggle for enabling online editing of pages 313 | - **[New]** Preparing for Raneto to be NPM-installable (see example/ for new usage) 314 | - **[New]** Codified Bower dependencies into bower.json 315 | - **[Upgraded]** Upgraded Bower modules in bower.json (current) 316 | - **[Upgraded]** Upgraded Node.js modules in package.json (current) 317 | - **[Removed]** ./bin/www script. Replace with "npm start" 318 | - **[Removed]** Unused modules 319 | 320 | ## 2015.10.10 / v0.7.0 321 | 322 | - **[New]** Added online editing of pages 323 | - contributed by **@matthiassb** 324 | - **[New]** Added HTTP Basic authentication 325 | - contributed by **@eighteyes** 326 | - **[New]** Added custom template layouts 327 | - contributed by **@zulfajuniadi** 328 | - **[Fixed]** Highlight.js language detection 329 | - contributed by **@thurloat** 330 | - **[Fixed]** Mobile design layout 331 | - contributed by **@adimitrov** 332 | - **[Fixed]** Added config.base_url in front of all assets 333 | - contributed by **@valeriangalliat** 334 | 335 | ## 2014.06.09 / v0.6.0 336 | 337 | - **[Changed]** Static files (e.g. images) can now be served from the content folder 338 | - **[Changed]** Removed commercial licensing 339 | 340 | ## 2014.06.05 / v0.5.0 341 | 342 | - **[New]** Changed app structure (now using raneto-core) 343 | - **[New]** Added a content_dir config option 344 | - **[New]** Added an analytics config option 345 | 346 | ## 2014.06.04 / v0.4.0 347 | 348 | - **[New]** Added %image_url% support to Markdown files 349 | - **[New]** Search queries are now highlighted in search results 350 | - **[Changed]** Fallback to generating title from filename if no meta title is set 351 | - **[Changed]** Moved route and error handlers to raneto.js 352 | - **[Changed]** Make search use "/" URL 353 | - **[Fixed]** Fixed \_\_dirname paths in Windows 354 | 355 | ## 2014.06.03 / v0.3.0 356 | 357 | - **[New]** Added masonry layout functionality to homepage 358 | - **[New]** Added commercial licensing 359 | 360 | ## 2014.06.02 / v0.2.0 361 | 362 | - **[New]** Added page and category sorting functionality 363 | - **[Fixed]** Added better handling of file reading errors in raneto.js 364 | 365 | ## 2014.06.02 / v0.1.2 366 | 367 | - **[Changed]** Changed default copyright in config.js 368 | 369 | ## 2014.06.02 / v0.1.1 370 | 371 | - **[New]** Added favicon 372 | - **[Fixed]** Error page 373 | 374 | ## 2014.05.30 / v0.1.0 375 | 376 | - Initial release 377 | 378 | ## Raneto Core Changelog 379 | 380 | ### 2015.04.22 - version 0.3.0 381 | 382 | - [New] Add support for symlinks in content dir 383 | 384 | ### 2014.06.05 - version 0.2.0 385 | 386 | - [New] Added formatting to doSearch results 387 | - [Changed] Move config options to overridable array 388 | 389 | ### 2014.06.04 - version 0.1.0 390 | 391 | - Initial release 392 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2024 Gilbert Pellegrom, Ryan Lelek, and Raneto Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make 2 | 3 | # The first command listed is the default 4 | .PHONY: default 5 | default: clean install; 6 | 7 | .PHONY: clean 8 | clean: 9 | 10 | # Remove Temporary Files 11 | rm -rf ./node_modules/; 12 | 13 | .PHONY: install 14 | install: 15 | 16 | # Install Node.js Modules 17 | npm install; 18 | 19 | .PHONY: test 20 | test: delint unit 21 | 22 | .PHONY: delint 23 | delint: 24 | npm run lint; 25 | 26 | .PHONY: unit 27 | unit: 28 | npm run unit; 29 | 30 | .PHONY: build 31 | build: 32 | echo "REMOVED"; 33 | 34 | .PHONY: start 35 | start: 36 | 37 | # Start HTTP Server 38 | node server.js; 39 | 40 | .PHONY: deploy 41 | deploy: 42 | 43 | # Install Node.js Modules (Production) 44 | npm install --omit=dev; 45 | 46 | .PHONY: d_build 47 | d_build: 48 | docker build -t raneto-local:latest .; 49 | 50 | .PHONY: d_run 51 | d_run: 52 | docker run --rm -it -p 3000:3000 raneto-local:latest; 53 | 54 | .PHONY: d_shell 55 | d_shell: 56 | docker run --rm -it raneto-local:latest /bin/sh; 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raneto [![Node.js CI](https://github.com/ryanlelek/Raneto/actions/workflows/nodejs.ci.yml/badge.svg)](https://github.com/ryanlelek/Raneto/actions/workflows/nodejs.ci.yml) [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B44615%2Fgithub.com%2Fryanlelek%2FRaneto.svg?type=shield&issueType=license)](https://app.fossa.com/projects/custom%2B44615%2Fgithub.com%2Fryanlelek%2FRaneto?ref=badge_shield&issueType=license) 2 | 3 | [![Raneto Logo](https://raw.githubusercontent.com/ryanlelek/Raneto/main/logo/logo_readme.png)](https://raneto.com/) 4 | 5 | [Raneto](https://raneto.com) is a free, open, simple Markdown-powered knowledge base for Node.js. 6 | [Find out more →](https://docs.raneto.com/what-is-raneto) 7 | [Live Demo →](https://docs.raneto.com/) 8 | [Documentation →](https://docs.raneto.com/) 9 | 10 | # Top Features 11 | 12 | - All content is file-based 13 | - Search file names and contents 14 | - Markdown editor in the browser 15 | - Login system for edit protection 16 | - Simple and Lightweight 17 | 18 | # Mailing List 19 | 20 | [Click here to join the mailing list](https://23afbd9f.sibforms.com/serve/MUIFAG1rmxtMH-Y_r96h_E7js7A7nUKcvP1fTNlIvKTMIzh7wD3u9SVbCiBc-Wo9TkSBADb2e3PEvAHWuXPMyUe_dEcdJsUihGQwDBX79nvS9bm3JYqyWOPjxacnexONo5yxNgHtnQKKG3JYtPS1LL1oejZ0rTchHzphtZuEbUJ3Hg6CimV69nbqhGKoNj-sPNhpvjSqgSIv3Zu0) for project news and important security alerts! 21 | 22 | # License Report 23 | 24 | [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B44615%2Fgithub.com%2Fryanlelek%2FRaneto.svg?type=large&issueType=license)](https://app.fossa.com/projects/custom%2B44615%2Fgithub.com%2Fryanlelek%2FRaneto?ref=badge_large&issueType=license) 25 | 26 | Temporarily Removed Packages 27 | 28 | ```bash 29 | "eslint-config-standard": "17.1.0", 30 | ``` 31 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 0.17.8 | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Email security@raneto.com for coordinating patches/releases 12 | -------------------------------------------------------------------------------- /app/core/contents.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'fs-extra'; 3 | import { glob } from 'glob'; 4 | import _ from 'underscore'; 5 | import _s from 'underscore.string'; 6 | import yaml from 'js-yaml'; 7 | import utils from './utils.js'; 8 | import content_processors from '../functions/content_processors.js'; 9 | 10 | // TODO: Scan on start/change, not on every request 11 | async function handler(activePageSlug, config) { 12 | activePageSlug = activePageSlug || ''; 13 | const baseSlug = activePageSlug.split(/[\\/]/).slice(0, -1).join('/'); 14 | const contentDir = utils.normalizeDir(path.normalize(config.content_dir)); 15 | 16 | // TODO: Fix extra trailing / 17 | const files = await glob(`${contentDir}**/*`); 18 | const filesProcessed = []; 19 | 20 | filesProcessed.push({ 21 | slug: '.', 22 | title: '', 23 | show_on_home: true, 24 | show_on_menu: true, 25 | is_index: true, 26 | active: baseSlug === '', 27 | class: 'category-index', 28 | sort: 0, 29 | files: [], 30 | }); 31 | 32 | const results = await Promise.all( 33 | files.map((filePath) => 34 | processFile(config, activePageSlug, contentDir, filePath), 35 | ), 36 | ); 37 | 38 | for (const result of results) { 39 | if (result && result.is_directory) { 40 | filesProcessed.push(result); 41 | } else if (result && result.is_directory === false) { 42 | const dirSlug = path.dirname(result.slug); 43 | const parent = _.find(filesProcessed, (item) => item.slug === dirSlug); 44 | if (parent) { 45 | parent.files.push(result); 46 | } else if (config.debug) { 47 | console.log('Content ignored', result.slug); 48 | } 49 | } 50 | } 51 | 52 | const sortedFiles = _.sortBy(filesProcessed, (cat) => cat.sort); 53 | sortedFiles.forEach((category) => { 54 | category.files = _.sortBy(category.files, (file) => file.sort); 55 | }); 56 | 57 | return sortedFiles; 58 | } 59 | 60 | async function processFile(config, activePageSlug, contentDir, filePath) { 61 | const content_dir = path.normalize(contentDir); 62 | const page_sort_meta = config.page_sort_meta || ''; 63 | const category_sort = config.category_sort || false; 64 | const shortPath = path.normalize(filePath).replace(content_dir, '').trim(); 65 | const fileSlug = shortPath.split('\\').join('/'); 66 | const stat = await fs.stat(filePath); 67 | 68 | if (stat.isDirectory()) { 69 | let sort = 0; 70 | // ignore directories that has an ignore file under it 71 | const ignoreFile = `${contentDir + shortPath}/ignore`; 72 | 73 | const ignoreExists = await fs.lstat(ignoreFile).then( 74 | (stat) => stat.isFile(), 75 | () => {}, 76 | ); 77 | if (ignoreExists) { 78 | if (config.debug) { 79 | console.log('Directory ignored', contentDir + shortPath); 80 | } 81 | 82 | return null; 83 | } 84 | 85 | let dirMetadata = {}; 86 | try { 87 | const metaFile = await fs.readFile( 88 | path.join(contentDir, shortPath, 'meta'), 89 | ); 90 | dirMetadata = content_processors.cleanObjectStrings( 91 | yaml.load(metaFile.toString('utf-8')), 92 | ); 93 | } catch (e) { 94 | if (config.debug) { 95 | console.log('No meta file for', contentDir + shortPath); 96 | } 97 | } 98 | 99 | if (category_sort && !dirMetadata.sort) { 100 | try { 101 | const sortFile = await fs.readFile( 102 | path.join(contentDir, shortPath, 'sort'), 103 | ); 104 | sort = parseInt(sortFile.toString('utf-8'), 10); 105 | } catch (e) { 106 | if (config.debug) { 107 | console.log('No sort file for', contentDir + shortPath); 108 | } 109 | } 110 | } 111 | 112 | return { 113 | slug: fileSlug, 114 | title: 115 | dirMetadata.title || _s.titleize(_s.humanize(path.basename(shortPath))), 116 | show_on_home: dirMetadata.show_on_home 117 | ? dirMetadata.show_on_home === 'true' 118 | : config.show_on_home_default, 119 | is_index: false, 120 | is_directory: true, 121 | show_on_menu: dirMetadata.show_on_menu 122 | ? dirMetadata.show_on_menu === 'true' 123 | : config.show_on_menu_default, 124 | active: activePageSlug.startsWith(`/${fileSlug}`), 125 | class: `category-${content_processors.cleanString(fileSlug)}`, 126 | sort: dirMetadata.sort || sort, 127 | description: dirMetadata.description || '', 128 | files: [], 129 | }; 130 | } 131 | 132 | if (stat.isFile() && path.extname(shortPath) === '.md') { 133 | try { 134 | const file = await fs.readFile(filePath); 135 | let slug = fileSlug; 136 | let pageSort = 0; 137 | 138 | if (fileSlug.indexOf('index.md') > -1) { 139 | slug = slug.replace('index.md', ''); 140 | } 141 | 142 | slug = slug.replace('.md', '').trim(); 143 | 144 | const meta = content_processors.processMeta(file.toString('utf-8')); 145 | 146 | if (page_sort_meta && meta[page_sort_meta]) { 147 | pageSort = parseInt(meta[page_sort_meta], 10); 148 | } 149 | 150 | return { 151 | slug, 152 | title: meta.title ? meta.title : content_processors.slugToTitle(slug), 153 | show_on_home: meta.show_on_home 154 | ? meta.show_on_home === 'true' 155 | : config.show_on_home_default, 156 | is_directory: false, 157 | show_on_menu: meta.show_on_menu 158 | ? meta.show_on_menu === 'true' 159 | : config.show_on_menu_default, 160 | active: activePageSlug.trim() === `/${slug}`, 161 | sort: pageSort, 162 | }; 163 | } catch (e) { 164 | if (config.debug) { 165 | console.log(e); 166 | } 167 | } 168 | } 169 | } 170 | 171 | export default handler; 172 | -------------------------------------------------------------------------------- /app/core/language.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | import { readFileSync } from 'node:fs'; 3 | import path from 'node:path'; 4 | 5 | function language_load(locale_code) { 6 | // Path is relative to current working directory 7 | const lang_json = JSON.parse( 8 | readFileSync(path.join('app', 'translations', locale_code + '.json')), 9 | ); 10 | return lang_json; 11 | } 12 | 13 | export default language_load; 14 | -------------------------------------------------------------------------------- /app/core/lunr.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | import lunr from 'lunr'; 3 | import lunr_stemmer from 'lunr-languages/lunr.stemmer.support.js'; 4 | import lunr_multi from 'lunr-languages/lunr.multi.js'; 5 | import lunr_tinyseg from 'lunr-languages/tinyseg.js'; 6 | // Languages 7 | import lunr_ru from 'lunr-languages/lunr.ru.js'; 8 | // TODO: Add more languages, "ru" was the only found config for "searchExtraLanguages" 9 | 10 | let instance = null; 11 | let stemmers = null; 12 | 13 | function getLunr(config) { 14 | if (instance === null) { 15 | instance = lunr; 16 | lunr_stemmer(instance); 17 | lunr_multi(instance); 18 | lunr_tinyseg(instance); 19 | config.searchExtraLanguages.forEach((lang) => { 20 | if (lang === 'ru') { 21 | lunr_ru(instance); 22 | } 23 | }); 24 | } 25 | return instance; 26 | } 27 | 28 | function getStemmers(config) { 29 | if (stemmers === null) { 30 | const languages = ['en'].concat(config.searchExtraLanguages); 31 | stemmers = getLunr(config).multiLanguage.apply(null, languages); 32 | } 33 | return stemmers; 34 | } 35 | 36 | export default { 37 | getLunr, 38 | getStemmers, 39 | }; 40 | -------------------------------------------------------------------------------- /app/core/page.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'fs-extra'; 3 | import _s from 'underscore.string'; 4 | import { marked } from 'marked'; 5 | import utils from './utils.js'; 6 | import content_processors from '../functions/content_processors.js'; 7 | 8 | async function handler(filePath, config) { 9 | const contentDir = utils.normalizeDir(path.normalize(config.content_dir)); 10 | 11 | try { 12 | const file = await fs.readFile(filePath); 13 | let slug = utils.getSlug(filePath, contentDir); 14 | 15 | if (slug.indexOf('index.md') > -1) { 16 | slug = slug.replace('index.md', ''); 17 | } 18 | slug = slug.replace('.md', '').trim(); 19 | 20 | const meta = content_processors.processMeta(file.toString('utf-8')); 21 | const content = content_processors.processVars( 22 | content_processors.stripMeta(file.toString('utf-8')), 23 | config, 24 | ); 25 | 26 | // Render Markdown 27 | marked.use({ 28 | // Removed in v8.x 29 | // https://github.com/markedjs/marked/releases/tag/v8.0.0 30 | // mangle: false, 31 | // headerIds: false, 32 | }); 33 | const body = marked(content); 34 | const title = meta.title 35 | ? meta.title 36 | : content_processors.slugToTitle(slug); 37 | const excerpt = _s.prune( 38 | _s.stripTags(_s.unescapeHTML(body)), 39 | config.excerpt_length || 400, 40 | ); 41 | 42 | return { 43 | slug, 44 | title, 45 | body, 46 | excerpt, 47 | }; 48 | } catch (e) { 49 | if (config.debug) { 50 | console.log(e); 51 | } 52 | return null; 53 | } 54 | } 55 | 56 | export default handler; 57 | -------------------------------------------------------------------------------- /app/core/search.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { glob } from 'glob'; 3 | import content_processors from '../functions/content_processors.js'; 4 | import utils from './utils.js'; 5 | import page_handler from './page.js'; 6 | import lunr from './lunr.js'; 7 | 8 | async function handler(query, config) { 9 | const contentDir = utils.normalizeDir(path.normalize(config.content_dir)); 10 | const rawDocuments = await glob(`${contentDir}**/*.md`); 11 | const potentialDocuments = await Promise.all( 12 | rawDocuments.map((filePath) => 13 | content_processors.extractDocument(contentDir, filePath, config.debug), 14 | ), 15 | ); 16 | const documents = potentialDocuments.filter((doc) => doc !== null); 17 | 18 | const lunrInstance = lunr.getLunr(config); 19 | const idx = lunrInstance(function () { 20 | this.use(lunr.getStemmers(config)); 21 | this.field('title'); 22 | this.field('body'); 23 | this.ref('id'); 24 | documents.forEach((doc) => this.add(doc), this); 25 | }); 26 | 27 | const results = idx.search(query); 28 | 29 | const searchResults = await Promise.all( 30 | results.map(async (result) => { 31 | const processed = await processSearchResult( 32 | contentDir, 33 | config, 34 | query, 35 | result, 36 | ); 37 | return processed; 38 | }), 39 | ); 40 | 41 | return searchResults; 42 | } 43 | 44 | async function processSearchResult(contentDir, config, query, result) { 45 | // Removed 46 | // contentDir + 47 | const page = await page_handler(result.ref, config); 48 | // TODO: Improve handling 49 | if (page && page.excerpt) { 50 | page.excerpt = page.excerpt.replace( 51 | new RegExp(`(${query})`, 'gim'), 52 | '$1', 53 | ); 54 | } 55 | return page; 56 | } 57 | 58 | export default handler; 59 | -------------------------------------------------------------------------------- /app/core/utils.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | import fs from 'fs-extra'; 3 | import moment from 'moment'; 4 | 5 | const normalizeDir = (dir) => dir.replace(/\\/g, '/'); 6 | const getSlug = (filePath, contentDir) => 7 | normalizeDir(filePath).replace(normalizeDir(contentDir), '').trim(); 8 | 9 | async function getLastModified(config, meta, file_path) { 10 | if (typeof meta.modified !== 'undefined') { 11 | return moment(meta.modified).format(config.datetime_format); 12 | } 13 | const { mtime } = await fs.lstat(file_path); 14 | return moment(mtime).format(config.datetime_format); 15 | } 16 | 17 | export default { 18 | normalizeDir, 19 | getLastModified, 20 | getSlug, 21 | }; 22 | -------------------------------------------------------------------------------- /app/functions/build_nested_pages.js: -------------------------------------------------------------------------------- 1 | function build_nested_pages(pages) { 2 | const result = []; 3 | let i = pages.length; 4 | 5 | while (i--) { 6 | if (pages[i].slug.split('/').length > 1) { 7 | const parent = find_by_slug(pages, pages[i]); 8 | if (parent) { 9 | parent.files.unshift(pages[i]); 10 | } 11 | } else { 12 | result.unshift(pages[i]); 13 | } 14 | } 15 | 16 | return result; 17 | } 18 | 19 | function find_by_slug(pages, page) { 20 | return pages.find( 21 | (element) => element.slug === page.slug.split('/').slice(0, -1).join('/'), 22 | ); 23 | } 24 | 25 | // Exports 26 | export default build_nested_pages; 27 | -------------------------------------------------------------------------------- /app/functions/content_processors.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | import path from 'node:path'; 3 | import fs from 'fs-extra'; 4 | import _ from 'underscore'; 5 | import _s from 'underscore.string'; 6 | import yaml from 'js-yaml'; 7 | 8 | // Regex for page meta (considers Byte Order Mark \uFEFF in case there's one) 9 | // Look for the the following header formats at the beginning of the file: 10 | // /* 11 | // {header string} 12 | // */ 13 | // or 14 | // --- 15 | // {header string} 16 | // --- 17 | // TODO: DEPRECATED Non-YAML 18 | const _metaRegex = /^\uFEFF?\/\*([\s\S]*?)\*\//i; 19 | const _metaRegexYaml = /^\uFEFF?---([\s\S]*?)---/i; 20 | 21 | function cleanString(str, use_underscore) { 22 | const u = use_underscore || false; 23 | str = str.replace(/\//g, ' ').trim(); 24 | if (u) { 25 | return _s.underscored(str); 26 | } 27 | return _s.trim(_s.dasherize(str), '-'); 28 | } 29 | 30 | // Clean object strings. 31 | function cleanObjectStrings(obj) { 32 | const cleanObj = {}; 33 | for (const field in obj) { 34 | if (_.has(obj, field)) { 35 | cleanObj[cleanString(field, true)] = `${obj[field]}`.trim(); 36 | } 37 | } 38 | return cleanObj; 39 | } 40 | 41 | // Convert a slug to a title 42 | function slugToTitle(slug) { 43 | slug = slug.replace('.md', '').trim(); 44 | return _s.titleize(_s.humanize(path.basename(slug))); 45 | } 46 | 47 | // Strip meta from Markdown content 48 | function stripMeta(markdownContent) { 49 | switch (true) { 50 | case _metaRegex.test(markdownContent): 51 | return markdownContent.replace(_metaRegex, '').trim(); 52 | case _metaRegexYaml.test(markdownContent): 53 | return markdownContent.replace(_metaRegexYaml, '').trim(); 54 | default: 55 | return markdownContent.trim(); 56 | } 57 | } 58 | 59 | // Get meta information from Markdown content 60 | function processMeta(markdownContent) { 61 | let meta = {}; 62 | let metaArr; 63 | let metaString; 64 | let metas; 65 | 66 | let yamlObject; 67 | 68 | switch (true) { 69 | case _metaRegex.test(markdownContent): 70 | metaArr = markdownContent.match(_metaRegex); 71 | metaString = metaArr ? metaArr[1].trim() : ''; 72 | 73 | if (metaString) { 74 | metas = metaString.match(/(.*): (.*)/gi); 75 | metas.forEach((item) => { 76 | const parts = item.split(': '); 77 | if (parts[0] && parts[1]) { 78 | meta[cleanString(parts[0], true)] = parts[1].trim(); 79 | } 80 | }); 81 | } 82 | break; 83 | 84 | case _metaRegexYaml.test(markdownContent): 85 | metaArr = markdownContent.match(_metaRegexYaml); 86 | metaString = metaArr ? metaArr[1].trim() : ''; 87 | yamlObject = yaml.load(metaString); 88 | meta = cleanObjectStrings(yamlObject); 89 | break; 90 | 91 | default: 92 | // No meta information 93 | } 94 | 95 | return meta; 96 | } 97 | 98 | // Replace content variables in Markdown content 99 | function processVars(markdownContent, config) { 100 | if (config.variables && Array.isArray(config.variables)) { 101 | config.variables.forEach((v) => { 102 | markdownContent = markdownContent.replace( 103 | new RegExp(`%${v.name}%`, 'g'), 104 | v.content, 105 | ); 106 | }); 107 | } 108 | if (config.base_url !== undefined) { 109 | markdownContent = markdownContent.replace(/%base_url%/g, config.base_url); 110 | } 111 | if (config.image_url !== undefined) { 112 | markdownContent = markdownContent.replace(/%image_url%/g, config.image_url); 113 | } 114 | return markdownContent; 115 | } 116 | 117 | async function extractDocument(contentDir, filePath, debug) { 118 | try { 119 | const file = await fs.readFile(filePath); 120 | const meta = processMeta(file.toString('utf-8')); 121 | 122 | const id = filePath.replace(contentDir, '').trim(); 123 | const title = meta.title ? meta.title : slugToTitle(id); 124 | const body = file.toString('utf-8'); 125 | 126 | return { id, title, body }; 127 | } catch (e) { 128 | if (debug) { 129 | console.log(e); 130 | } 131 | return null; 132 | } 133 | } 134 | 135 | export default { 136 | cleanString, 137 | cleanObjectStrings, 138 | extractDocument, 139 | slugToTitle, 140 | stripMeta, 141 | processMeta, 142 | processVars, 143 | }; 144 | -------------------------------------------------------------------------------- /app/functions/create_meta_info.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | import yaml from 'js-yaml'; 3 | 4 | // Returns an empty string if all input strings are empty 5 | function create_meta_info(meta_title, meta_description, meta_sort) { 6 | const yamlDocument = {}; 7 | const meta_info_is_present = meta_title || meta_description || meta_sort; 8 | 9 | if (meta_info_is_present) { 10 | if (meta_title) { 11 | yamlDocument.Title = meta_title; 12 | } 13 | if (meta_description) { 14 | yamlDocument.Description = meta_description; 15 | } 16 | if (meta_sort) { 17 | yamlDocument.Sort = parseInt(meta_sort, 10); 18 | } 19 | 20 | return `---\n${yaml.dump(yamlDocument)}---\n`; 21 | } 22 | return '---\n---\n'; 23 | } 24 | 25 | // Exports 26 | export default create_meta_info; 27 | -------------------------------------------------------------------------------- /app/functions/get_filepath.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | import path from 'node:path'; 3 | import sanitizeFilename from 'sanitize-filename'; 4 | import sanitize from './sanitize.js'; 5 | 6 | function get_filepath(p) { 7 | // Default 8 | let filepath = p.content; 9 | 10 | // Add Category 11 | if (p.category) { 12 | filepath += `/${sanitizeFilename(sanitize(p.category))}`; 13 | } 14 | 15 | // Add File Name 16 | if (p.filename) { 17 | filepath += `/${sanitizeFilename(sanitize(p.filename))}`; 18 | } 19 | 20 | // Normalize 21 | filepath = path.normalize(filepath); 22 | 23 | return filepath; 24 | } 25 | 26 | // Exports 27 | export default get_filepath; 28 | -------------------------------------------------------------------------------- /app/functions/remove_image_content_directory.js: -------------------------------------------------------------------------------- 1 | function remove_image_content_directory(config, pageList) { 2 | for (let i = 0; i < pageList.length; i++) { 3 | if (pageList[i].slug === config.image_url.replace(/\//g, '')) { 4 | pageList.splice(i, 1); 5 | } 6 | } 7 | return pageList; 8 | } 9 | 10 | // Exports 11 | export default remove_image_content_directory; 12 | -------------------------------------------------------------------------------- /app/functions/sanitize.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | import validator from 'validator'; 3 | 4 | // Settings 5 | const invalidChars = '&\'"/><'; 6 | 7 | // TODO: Add Test 8 | function sanitizer(str) { 9 | str = validator.blacklist(str, invalidChars); 10 | str = validator.trim(str); 11 | str = validator.escape(str); 12 | return str; 13 | } 14 | 15 | // Exports 16 | export default sanitizer; 17 | -------------------------------------------------------------------------------- /app/functions/sanitize_markdown.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | // import validator from 'validator'; 3 | 4 | // Sanitize Content 5 | // This will disallow