├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── barely-test.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── barely ├── __init__.py ├── blueprints │ ├── blank │ │ └── config.yaml │ ├── blog │ │ ├── COPY-TO-CONFIG.yaml │ │ ├── blog │ │ │ ├── 2022-04-01-building-a-blog-with-barely │ │ │ │ └── post.md │ │ │ └── page.md │ │ ├── metadata.yaml │ │ ├── page.md │ │ ├── res │ │ │ ├── favicon.ico │ │ │ ├── highlight │ │ │ │ └── one-dark.css │ │ │ ├── icons │ │ │ │ ├── facebook.svg │ │ │ │ ├── reddit.svg │ │ │ │ ├── share.svg │ │ │ │ ├── twitter.svg │ │ │ │ └── ycombinator.svg │ │ │ ├── socialcover.png │ │ │ ├── style.sass │ │ │ └── theme.js │ │ ├── rss │ │ │ └── rss.md │ │ └── templates │ │ │ ├── overview.html │ │ │ ├── page.html │ │ │ ├── partials │ │ │ ├── base.html │ │ │ ├── preview.html │ │ │ └── socials.html │ │ │ ├── post.html │ │ │ ├── rss.html │ │ │ └── tag.html │ └── default │ │ ├── config.yaml │ │ ├── metadata.yaml │ │ ├── page.md │ │ ├── res │ │ ├── favicon.ico │ │ └── style.sass │ │ └── templates │ │ ├── page.html │ │ └── partials │ │ └── base.html ├── cli.py ├── common │ ├── __init__.py │ ├── config.py │ ├── decorators.py │ └── empty_config.yaml ├── core │ ├── ChangeTracker.py │ ├── EventHandler.py │ ├── ProcessingPipeline.py │ └── __init__.py ├── plugins │ ├── PluginManager.py │ ├── __init__.py │ ├── backup │ │ ├── LocalBackup │ │ │ ├── __init__.py │ │ │ ├── localbackup.py │ │ │ └── test_localbackup.py │ │ ├── __init__.py │ │ └── git │ │ │ ├── __init__.py │ │ │ ├── git.py │ │ │ └── test_git.py │ ├── content │ │ ├── AutoSEO │ │ │ ├── __init__.py │ │ │ ├── autoseo.py │ │ │ └── test_autoseo.py │ │ ├── Collections │ │ │ ├── __init__.py │ │ │ ├── collections.py │ │ │ └── test_collections.py │ │ ├── Forms │ │ │ ├── __init__.py │ │ │ ├── forms.py │ │ │ └── test_forms.py │ │ ├── Gallery │ │ │ ├── __init__.py │ │ │ ├── gallery.py │ │ │ └── test_gallery.py │ │ ├── Highlight │ │ │ ├── __init__.py │ │ │ ├── highlight.py │ │ │ └── test_highlight.py │ │ ├── Minify │ │ │ ├── __init__.py │ │ │ ├── minify.py │ │ │ └── test_minify.py │ │ ├── Pixelizer │ │ │ ├── __init__.py │ │ │ ├── pixelizer.py │ │ │ └── test_pixelizer.py │ │ ├── ReadingTime │ │ │ ├── __init__.py │ │ │ ├── readingtime.py │ │ │ └── test_readingtime.py │ │ ├── Timestamps │ │ │ ├── __init__.py │ │ │ ├── test_timestamps.py │ │ │ └── timestamps.py │ │ ├── ToC │ │ │ ├── __init__.py │ │ │ ├── test_toc.py │ │ │ └── toc.py │ │ └── __init__.py │ └── publication │ │ ├── __init__.py │ │ └── sftp │ │ ├── __init__.py │ │ ├── sftp.py │ │ └── test_sftp.py └── tests │ ├── __init__.py │ ├── ressources │ ├── EventHandler │ │ ├── affected │ │ │ ├── base.md │ │ │ ├── extendsdeeper.md │ │ │ ├── left.extendsbase.md │ │ │ ├── left.left.extendsbase.md │ │ │ ├── left.left.extendschild.md │ │ │ ├── left.parentless.md │ │ │ ├── left.right.completelyalone.md │ │ │ ├── right.left.completelyalone.md │ │ │ ├── right.left.deeper.md │ │ │ ├── right.right.extendsparentless.md │ │ │ └── templates │ │ │ │ ├── base.html │ │ │ │ ├── extendsdeeper.html │ │ │ │ ├── left │ │ │ │ ├── extendsbase.html │ │ │ │ ├── left │ │ │ │ │ ├── extendsbase.html │ │ │ │ │ └── extendschild.html │ │ │ │ ├── parentless.html │ │ │ │ └── right │ │ │ │ │ └── completelyalone.html │ │ │ │ └── right │ │ │ │ ├── left │ │ │ │ ├── completelyalone.html │ │ │ │ └── deeper.html │ │ │ │ └── right │ │ │ │ └── extendsparentless.html │ │ ├── force │ │ │ ├── config.yaml │ │ │ ├── dir │ │ │ │ ├── _subpage │ │ │ │ │ ├── subimage.jpg │ │ │ │ │ └── subpage.md │ │ │ │ ├── image.png │ │ │ │ └── page.md │ │ │ ├── file.txt │ │ │ ├── metadata.yaml │ │ │ └── templates │ │ │ │ └── template.html │ │ ├── getparent │ │ │ ├── _sub │ │ │ │ └── child.md │ │ │ ├── none │ │ │ │ └── none │ │ │ │ │ └── parentless.md │ │ │ ├── parent.md │ │ │ └── x_parent.md │ │ └── type │ │ │ ├── binary.mp4 │ │ │ ├── file.css │ │ │ └── notype │ ├── PluginManager │ │ ├── content │ │ │ ├── P1.py │ │ │ └── P2.py │ │ ├── empty │ │ │ └── collateral │ │ └── other │ │ │ ├── P3Dir │ │ │ └── P3.py │ │ │ └── P4.py │ ├── Plugins │ │ ├── AutoSEO │ │ │ ├── a.png │ │ │ └── b.jpg │ │ └── Gallery │ │ │ ├── a.png │ │ │ ├── b.jpg │ │ │ └── c.png │ ├── ProcessingPipeline │ │ ├── content_files │ │ │ ├── EMPTY.md │ │ │ ├── MULTI_YAML.md │ │ │ ├── ONE_YAML_ONE_MD.md │ │ │ ├── ONLY_MD.md │ │ │ └── ONLY_YAML.md │ │ ├── delete │ │ │ ├── dir │ │ │ │ └── collateral │ │ │ └── file.txt │ │ ├── move │ │ │ ├── fro.txt │ │ │ ├── fro │ │ │ │ └── collateral │ │ │ ├── fro2.txt │ │ │ └── fro2 │ │ │ │ └── collateral │ │ ├── pipes │ │ │ ├── _subpage │ │ │ │ └── subpage_dev.md │ │ │ ├── generic_dev.txt │ │ │ ├── image_dev.png │ │ │ ├── page_dev.md │ │ │ ├── page_with_child_dev.md │ │ │ ├── subpage_dev.md │ │ │ └── text_dev.txt │ │ ├── subpages │ │ │ └── _subpage │ │ │ │ └── sub.md │ │ ├── template.md │ │ ├── template.subtemplate.md │ │ ├── templates │ │ │ ├── page_dev.html │ │ │ ├── page_with_child_dev.html │ │ │ ├── subpage_dev.html │ │ │ └── template.html │ │ ├── test_load.png │ │ ├── test_read.md │ │ └── test_save.png │ ├── config.yaml │ └── empty │ │ └── empty │ ├── test_ChangeTracker.py │ ├── test_EventHandler.py │ ├── test_PluginManager.py │ └── test_ProcessingPipeline.py ├── docs ├── README.md ├── about.md ├── barely-demo.gif ├── blueprints.md ├── detailed-overview.md ├── getting-started.md ├── lighthouse.md ├── logo.png ├── modular-pages.md ├── plugins.md └── plugins │ ├── autoseo.md │ ├── collections.md │ ├── forms.md │ ├── gallery.md │ ├── git.md │ ├── highlight.md │ ├── localbackup.md │ ├── minify.md │ ├── pixelizer.md │ ├── readingtime.md │ ├── sftp.md │ ├── timestamps.md │ └── toc.md ├── flake.lock ├── flake.nix ├── setup.cfg └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Enter command '...' 16 | 2. Do the following action: '....' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Desktop (please complete the following information):** 26 | - OS: [e.g. Ubuntu] 27 | - Release [e.g. 1.0.0] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/barely-test.yml: -------------------------------------------------------------------------------- 1 | name: barely test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 3.9 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: 3.9 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install . 24 | - name: Run Testsuite 25 | run: | 26 | barely test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # barely-specific 2 | blueprints/devroot/config.yaml 3 | barely/tests/ref/backups/backups/ 4 | barely/tests/ref/backups/webroot/ 5 | barely/tests/ref/backups/devroot/ 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | cover/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | .pybuilder/ 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | # For a library or package, you might want to ignore these files since the code is 93 | # intended to run in multiple environments; otherwise, check them in: 94 | # .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 104 | __pypackages__/ 105 | 106 | # Celery stuff 107 | celerybeat-schedule 108 | celerybeat.pid 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | 140 | # pytype static type analyzer 141 | .pytype/ 142 | 143 | # Cython debug symbols 144 | cython_debug/ 145 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | - nothing 9 | 10 | ## [1.2.2] - 2025-04-16 11 | ### Added 12 | - `--no-aftermath` / `-n` option for `barely rebuild` to skip aftermath question 13 | 14 | ### Removed 15 | - AutoSummary plugin, due to packaging difficulties and unidiomatic installation 16 | 17 | ## [1.2.1] - 2025-04-16 18 | ### Added 19 | - Nix Flake containing both a dev shell and the barely package 20 | 21 | ## [1.2.0] - 2025-04-16 22 | ### Changed 23 | - include the full `meta` field of collectibles in the exhibition list 24 | - use any `meta` field for sorting collections 25 | 26 | ### Fixed 27 | - PIL naming (ANTIALIAS->LANCZOS) 28 | 29 | ## [1.1.4] - 2022-04-07 30 | ### Added 31 | - new "blog" blueprint - read about it here: [https://notablog.io/blog/2022-04-01-building-a-blog-with-barely/](https://notablog.io/blog/2022-04-01-building-a-blog-with-barely/) 32 | 33 | ## [1.1.2] - 2022-04-05 34 | ### Added 35 | - "publish: false" in a page can disable rendering of a page. Can also be used as a global toggle 36 | - Collections: added ORDER_KEY and ORDER_REVERSE options. Can be used to configure the order of posts within collection pages. 37 | 38 | ### Changed 39 | - Collections: "created" timestamps take precedence over "edited" timestamps 40 | 41 | ## [1.1.0] - 2022-04-03 42 | ### Added 43 | - Collections: the OVERVIEW_CONTENT field allows to specify a markdown file to be used for the Collection overview page's content 44 | 45 | ### Fixed 46 | - no longer ignores "meta" fields already set on a page. Previously they were overridden in the meta parsing process 47 | - Collections: if a page belonging to a collection was not modified after a rebuild, it would not be passed through the plugin pipeline. Among other side effects, this did not allow for Timestamp- and ReadingTime-integration for post previews 48 | - Timestamp: no longer panics if a file vanishes 49 | - ToC: indented ToC HTML was not accessibility friendly 50 | 51 | ### Changed 52 | - ReadingTime: if the plugin was configured with WPM_FAST and WPM_SLOW values being identical, or if the text was very short, the fast and slow estimate could be identical. In this case, the plugin now simply shows "0" instead of "0 - 0" (for example) 53 | - the "content_raw" field utilized by some plugins now only contains the unparsed markdown content, where previously it also included the yaml headers 54 | 55 | ## [1.0.5] - 2022-02-23 56 | ### Fixed 57 | - autoSEO: fixed double "/" issue in image URLs 58 | 59 | ### Changed 60 | - silently ignores FileNotFound errors instead of throwing an exception, since usually, a temp file is at fault 61 | 62 | ## [1.0.4] - 2021-09-06 63 | ### Added 64 | - "--desktop" flag for lighthouse (default is mobile) 65 | 66 | ### Fixed 67 | - autoSEO: no longer crashes when no image can be found; ignores modular subpages 68 | - pixelizer: save original image without blowing up its size 69 | - minify: configuring minify no longer disables it 70 | - toc: don't generate tocs for modular subpages 71 | - highlight: regex non-greedy, previously wrong behaviour when multiple code blocks on a page; eliminated duplicate stylesheet generation 72 | 73 | ### Changed 74 | - enabled git plugin by default 75 | - higher resilience against errors (non-critical errors get logged instead of excepted) 76 | - autoSEO: use site_name as fallback for title 77 | - highlight: accepts more conventional / markdown-style lexer notation; un-escape code before highlighting 78 | - collections: category & overview pages now passed through enabled plugins; use summary as preview, if it exists; preview-image does no longer have to be in the same directory 79 | 80 | ## [1.0.3] - 2021-09-01 81 | ### Fixed 82 | - faulty system blueprints path made barely unable to find any blueprints 83 | 84 | ### Changed 85 | - to ensure at least somewhat "unique" alt-tags in galleries, include the number-position of the image in the gallery 86 | - original/fallback images will no longer be processed by PIL in the Pixelizer plugin, but rather just be copied; their filesizes got blown up before, and the step was needless anyways 87 | 88 | ## [1.0.2] - 2021-08-27 89 | ### Changed 90 | - clean up raw_content before AutoSummary consumes it 91 | 92 | ### Fixed 93 | - robots.txt no longer weirdly indented 94 | - sitemap generation now works after fixing a typo (hmtl -> html) 95 | 96 | ## [1.0.0] - 2021-08-26 97 | ### Added 98 | - lighthouse CLI integration 99 | - AutoSEO plugin 100 | - AutoSummary plugin 101 | - Gallery plugin 102 | - Minify plugin 103 | - Pixelizer plugin 104 | - global `-d` debugging-flag 105 | - this changelog! 106 | 107 | ### Changed 108 | - moved from BETA to STABLE 109 | - switched version numbering scheme from `v_095` to more readable `v1.0.0` 110 | - proper logging instead of print() 111 | - simplified the default blueprint to make it more usable 112 | 113 | ### Fixed 114 | - various small performance improvements, largely due to eliminating duplicate function calls 115 | 116 | ### Removed 117 | - Minimizer plugin, obsolete thanks to Minify and Pixelizer 118 | 119 | ## [0.9.5] - 2021-07-19 120 | ### Added 121 | - `-l` flag for light rebuilds (no re-rendering of images, preserves webroot) 122 | - `-s` flag to start the live server after a rebuild 123 | 124 | ### Fixed 125 | - bugs preventing forms and collections from being compiled correctly 126 | - unability to render an item no longer crashes barely 127 | 128 | ## [0.9.0] - 2021-06-19 129 | Initial beta release 130 | 131 | [Unreleased]: https://github.com/charludo/barely/compare/v1.2.2...HEAD 132 | [1.2.2]: https://github.com/charludo/barely/compare/v1.2.1...v1.2.2 133 | [1.2.1]: https://github.com/charludo/barely/compare/v1.2.0...v1.2.1 134 | [1.2.0]: https://github.com/charludo/barely/compare/v1.1.4...v1.2.0 135 | [1.1.4]: https://github.com/charludo/barely/compare/v1.1.2...v1.1.4 136 | [1.1.2]: https://github.com/charludo/barely/compare/v1.1.0...v1.1.2 137 | [1.1.0]: https://github.com/charludo/barely/compare/v1.0.5...v1.1.0 138 | [1.0.5]: https://github.com/charludo/barely/compare/v1.0.4...v1.0.5 139 | [1.0.4]: https://github.com/charludo/barely/compare/v1.0.3...v1.0.4 140 | [1.0.3]: https://github.com/charludo/barely/compare/v1.0.2...v1.0.3 141 | [1.0.2]: https://github.com/charludo/barely/compare/v1.0.0...v1.0.2 142 | [1.0.0]: https://github.com/charludo/barely/compare/v_095...v1.0.0 143 | [0.9.5]: https://github.com/charludo/barely/compare/v_090...v_095 144 | [0.9.0]: https://github.com/charludo/barely/releases/tag/v_090 145 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions are what make the open source community such an amazing place. Any contributions you make are **very** much welcome! 4 | 5 | 1. Fork the Project 6 | 2. Create your Feature Branch (`git checkout -b feature/NewFeature`) 7 | 3. Commit your Changes (`git commit -m 'Add some NewFeature'`) 8 | 4. Push to the Branch (`git push origin feature/NewFeature`) 9 | 5. Open a Pull Request 10 | 11 | **If you have written a plugin or created a blueprint and think others might benefit, please do share via the same way!!** 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include barely * 2 | recursive-include docs * 3 | recursive-include blueprints * 4 | include LICENSE 5 | include README.md 6 | -------------------------------------------------------------------------------- /barely/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/__init__.py -------------------------------------------------------------------------------- /barely/blueprints/blank/config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/blueprints/blank/config.yaml -------------------------------------------------------------------------------- /barely/blueprints/blog/COPY-TO-CONFIG.yaml: -------------------------------------------------------------------------------- 1 | TIMESTAMPS: 2 | FORMAT: "%Y-%m-%d" 3 | 4 | TOC: 5 | LIST_ELEMENT: ul 6 | MIN_DEPTH: 2 7 | 8 | HIGHLIGHT: 9 | ASSETS_DIR: res 10 | LEXER: Rust 11 | THEME: one-dark 12 | 13 | PIXELIZER: 14 | TARGETS: 15 | - lg 660 75 16 | - md 510 70 17 | - sm 360 65 18 | LAYOUTS: 19 | - "(max-width: 800px) 100vw" 20 | - "660px" 21 | 22 | AUTO_SUMMARY: 23 | KEYWORDS: true 24 | 25 | AUTO_SEO: 26 | PRIORITY: 991 27 | 28 | COLLECTIONS: 29 | PRIORITY: 990 30 | PAGE: tags 31 | OVERVIEW_TITLE: List of all tags 32 | OVERVIEW_TEMPLATE: overview.html 33 | COLLECTION_TEMPLATE: tag.html 34 | ORDER_KEY: date 35 | ORDER_REVERSE: true 36 | -------------------------------------------------------------------------------- /barely/blueprints/blog/blog/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: All Bloposts 3 | exhibits: 4 | - all 5 | --- 6 | -------------------------------------------------------------------------------- /barely/blueprints/blog/metadata.yaml: -------------------------------------------------------------------------------- 1 | site_url: https://default.com 2 | site_name: barely blog 3 | site_description: default description 4 | site_keywords: 5 | - python 6 | - barely 7 | favicon: /res/favicon.ico 8 | title_image: /res/socialcover.png 9 | blogrepo: yourrepo 10 | -------------------------------------------------------------------------------- /barely/blueprints/blog/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome! 3 | exhibits: 4 | - featured 5 | --- 6 | 7 | # Welcome! 8 | 9 | Welcome to this blog. At the moment, there's not much here . 10 | -------------------------------------------------------------------------------- /barely/blueprints/blog/res/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/blueprints/blog/res/favicon.ico -------------------------------------------------------------------------------- /barely/blueprints/blog/res/highlight/one-dark.css: -------------------------------------------------------------------------------- 1 | pre { line-height: 125%; } 2 | td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 3 | span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } 4 | td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 5 | span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } 6 | hl .hll { background-color: #ffffcc } 7 | hl { background: #282C34; color: #ABB2BF } 8 | hl .hlc { color: #7F848E } /* Comment */ 9 | hl .hlerr { color: #ABB2BF } /* Error */ 10 | hl .hlesc { color: #ABB2BF } /* Escape */ 11 | hl .hlg { color: #ABB2BF } /* Generic */ 12 | hl .hlk { color: #C678DD } /* Keyword */ 13 | hl .hll { color: #ABB2BF } /* Literal */ 14 | hl .hln { color: #E06C75 } /* Name */ 15 | hl .hlo { color: #56B6C2 } /* Operator */ 16 | hl .hlx { color: #ABB2BF } /* Other */ 17 | hl .hlp { color: #ABB2BF } /* Punctuation */ 18 | hl .hlch { color: #7F848E } /* Comment.Hashbang */ 19 | hl .hlcm { color: #7F848E } /* Comment.Multiline */ 20 | hl .hlcp { color: #7F848E } /* Comment.Preproc */ 21 | hl .hlcpf { color: #7F848E } /* Comment.PreprocFile */ 22 | hl .hlc1 { color: #7F848E } /* Comment.Single */ 23 | hl .hlcs { color: #7F848E } /* Comment.Special */ 24 | hl .hlgd { color: #ABB2BF } /* Generic.Deleted */ 25 | hl .hlge { color: #ABB2BF } /* Generic.Emph */ 26 | hl .hlgr { color: #ABB2BF } /* Generic.Error */ 27 | hl .hlgh { color: #ABB2BF } /* Generic.Heading */ 28 | hl .hlgi { color: #ABB2BF } /* Generic.Inserted */ 29 | hl .hlgo { color: #ABB2BF } /* Generic.Output */ 30 | hl .hlgp { color: #ABB2BF } /* Generic.Prompt */ 31 | hl .hlgs { color: #ABB2BF } /* Generic.Strong */ 32 | hl .hlgu { color: #ABB2BF } /* Generic.Subheading */ 33 | hl .hlgt { color: #ABB2BF } /* Generic.Traceback */ 34 | hl .hlkc { color: #E5C07B } /* Keyword.Constant */ 35 | hl .hlkd { color: #C678DD } /* Keyword.Declaration */ 36 | hl .hlkn { color: #C678DD } /* Keyword.Namespace */ 37 | hl .hlkp { color: #C678DD } /* Keyword.Pseudo */ 38 | hl .hlkr { color: #C678DD } /* Keyword.Reserved */ 39 | hl .hlkt { color: #E5C07B } /* Keyword.Type */ 40 | hl .hlld { color: #ABB2BF } /* Literal.Date */ 41 | hl .hlm { color: #D19A66 } /* Literal.Number */ 42 | hl .hls { color: #98C379 } /* Literal.String */ 43 | hl .hlna { color: #E06C75 } /* Name.Attribute */ 44 | hl .hlnb { color: #E5C07B } /* Name.Builtin */ 45 | hl .hlnc { color: #E5C07B } /* Name.Class */ 46 | hl .hlno { color: #E06C75 } /* Name.Constant */ 47 | hl .hlnd { color: #61AFEF } /* Name.Decorator */ 48 | hl .hlni { color: #E06C75 } /* Name.Entity */ 49 | hl .hlne { color: #E06C75 } /* Name.Exception */ 50 | hl .hlnf { color: #61AFEF; font-weight: bold } /* Name.Function */ 51 | hl .hlnl { color: #E06C75 } /* Name.Label */ 52 | hl .hlnn { color: #E06C75 } /* Name.Namespace */ 53 | hl .hlnx { color: #E06C75 } /* Name.Other */ 54 | hl .hlpy { color: #E06C75 } /* Name.Property */ 55 | hl .hlnt { color: #E06C75 } /* Name.Tag */ 56 | hl .hlnv { color: #E06C75 } /* Name.Variable */ 57 | hl .hlow { color: #56B6C2 } /* Operator.Word */ 58 | hl .hlp-Marker { color: #ABB2BF } /* Punctuation.Marker */ 59 | hl .hlw { color: #ABB2BF } /* Text.Whitespace */ 60 | hl .hlmb { color: #D19A66 } /* Literal.Number.Bin */ 61 | hl .hlmf { color: #D19A66 } /* Literal.Number.Float */ 62 | hl .hlmh { color: #D19A66 } /* Literal.Number.Hex */ 63 | hl .hlmi { color: #D19A66 } /* Literal.Number.Integer */ 64 | hl .hlmo { color: #D19A66 } /* Literal.Number.Oct */ 65 | hl .hlsa { color: #98C379 } /* Literal.String.Affix */ 66 | hl .hlsb { color: #98C379 } /* Literal.String.Backtick */ 67 | hl .hlsc { color: #98C379 } /* Literal.String.Char */ 68 | hl .hldl { color: #98C379 } /* Literal.String.Delimiter */ 69 | hl .hlsd { color: #98C379 } /* Literal.String.Doc */ 70 | hl .hls2 { color: #98C379 } /* Literal.String.Double */ 71 | hl .hlse { color: #98C379 } /* Literal.String.Escape */ 72 | hl .hlsh { color: #98C379 } /* Literal.String.Heredoc */ 73 | hl .hlsi { color: #98C379 } /* Literal.String.Interpol */ 74 | hl .hlsx { color: #98C379 } /* Literal.String.Other */ 75 | hl .hlsr { color: #98C379 } /* Literal.String.Regex */ 76 | hl .hls1 { color: #98C379 } /* Literal.String.Single */ 77 | hl .hlss { color: #98C379 } /* Literal.String.Symbol */ 78 | hl .hlbp { color: #E5C07B } /* Name.Builtin.Pseudo */ 79 | hl .hlfm { color: #56B6C2; font-weight: bold } /* Name.Function.Magic */ 80 | hl .hlvc { color: #E06C75 } /* Name.Variable.Class */ 81 | hl .hlvg { color: #E06C75 } /* Name.Variable.Global */ 82 | hl .hlvi { color: #E06C75 } /* Name.Variable.Instance */ 83 | hl .hlvm { color: #E06C75 } /* Name.Variable.Magic */ 84 | hl .hlil { color: #D19A66 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /barely/blueprints/blog/res/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /barely/blueprints/blog/res/icons/reddit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /barely/blueprints/blog/res/icons/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /barely/blueprints/blog/res/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /barely/blueprints/blog/res/icons/ycombinator.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /barely/blueprints/blog/res/socialcover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/blueprints/blog/res/socialcover.png -------------------------------------------------------------------------------- /barely/blueprints/blog/res/theme.js: -------------------------------------------------------------------------------- 1 | prev = localStorage.getItem("theme"); 2 | if (prev) { 3 | setTheme(prev); 4 | } 5 | 6 | var toggleLight = document.getElementById("toggle--light"); 7 | var toggleDark = document.getElementById("toggle--dark"); 8 | var toggleRust = document.getElementById("toggle--rust"); 9 | 10 | toggleLight.addEventListener("click", function(){setTheme("theme--light")}); 11 | toggleDark.addEventListener("click", function(){setTheme("theme--dark")}); 12 | toggleRust.addEventListener("click", function(){setTheme("theme--rust")}); 13 | 14 | function setTheme(t) { 15 | document.body.className = t; 16 | localStorage.setItem("theme", t); 17 | } 18 | 19 | function toast(a) { 20 | navigator.clipboard.writeText(a); 21 | var x = document.getElementById("snackbar"); 22 | x.className = "show"; 23 | setTimeout(function(){ x.className = x.className.replace("show", ""); }, 3000); 24 | } 25 | 26 | window.toast = toast; 27 | -------------------------------------------------------------------------------- /barely/blueprints/blog/rss/rss.md: -------------------------------------------------------------------------------- 1 | --- 2 | extension: xml 3 | exhibits: 4 | - all 5 | --- 6 | -------------------------------------------------------------------------------- /barely/blueprints/blog/templates/overview.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/base.html" %} 2 | 3 | {% block body %} 4 |

{{ title }}

5 | {% for collection in collections_list %} 6 | {% if collection["name"] != "all" and collection["name"] != "featured" %} 7 |

#{{ collection["name"] }} {{ [collection["size"]] }}

8 | {% endif %} 9 | {% endfor %} 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /barely/blueprints/blog/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/base.html" %} 2 | 3 | {% block body %} 4 | {{ content }} 5 | {% for exhibit in exhibits %} 6 | 7 | {% for collectible in exhibits[exhibit] %} 8 | {% include "partials/preview.html" %} 9 | {% endfor %} 10 | {% endfor %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /barely/blueprints/blog/templates/partials/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ seo_tags }} 5 | 6 | 7 | 8 | {% block extra_css %}{% endblock %} 9 | 10 | 11 |
12 | 25 | {% block body %} 26 | 27 | {% endblock %} 28 |
29 |
Copied link!
30 | {% block extra_js %}{% endblock %} 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /barely/blueprints/blog/templates/partials/preview.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ collectible["date"] }} / {{ collectible["reading_time"] }} min

3 |

{{ collectible["title"] }}

4 |

{{ collectible["preview"] }} [read more]

5 |
6 | -------------------------------------------------------------------------------- /barely/blueprints/blog/templates/partials/socials.html: -------------------------------------------------------------------------------- 1 |
2 |
copy link
3 | reddit 4 | y combinator 5 | twitter 6 | facebook 7 |
8 | -------------------------------------------------------------------------------- /barely/blueprints/blog/templates/post.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/base.html" %} 2 | 3 | {% block body %} 4 |
5 |

{{ created }}{% if ed and ed|string != created|string %} (ed. {{ ed }}){% endif %}{% if collections %} / #{{ collections[0] }}{% endif %} / 6 | {{ reading_time }} min 7 |

8 |

{{ title }}

9 | {% include "partials/socials.html" %} 10 |

{{ summary }}

11 |
12 | {% if toc %} 13 | Table of Contents: 14 | {{ toc }} 15 | {% endif %} 16 | {% if gitalong %} 17 |
18 |

All code examples are available in this GitHub repository!

19 |
20 | {% endif %} 21 |
22 | {{ content|replace(" 24 |
25 |

This is a 100% static website. Therefore, no direct commenting method is available. However, feel free to discuss this article on GitHub! 26 |

27 |
28 |
29 | {% if collections %} 30 | {% for collection in collections %} 31 | {% if collection != "all" and collection != "featured" %} 32 | #{{ collection }} 33 | {% endif %} 34 | {% endfor %} 35 | {% endif %} 36 |
37 | {% include "partials/socials.html" %} 38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /barely/blueprints/blog/templates/rss.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | {{ site_name }} 7 | {{ page_url }}/index.xml 8 | en-us 9 | 10 | {{ site_name }} 11 | {{ site_url }} 12 | {{ site_url }}{{ title_image }} 13 | 1200 14 | 630 15 | 16 | 17 | {{ site_description }} 18 | http://blogs.law.harvard.edu/tech/rss 19 | technology 20 | {% if exhibits %} 21 | {% if "all" in exhibits %} 22 | {% for item in exhibits["all"] %} 23 | 24 | {{ item.title }} 25 | {{ item.preview|safe }} 26 | {{ item.date }} 27 | {{ site_url }}{{ item.href }} 28 | {{ site_url }}{{ item.href }} 29 | 30 | 31 | {% endfor %} 32 | {% endif %} 33 | {% endif %} 34 | 35 | 36 | -------------------------------------------------------------------------------- /barely/blueprints/blog/templates/tag.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/base.html" %} 2 | 3 | {% block body %} 4 |

Blogposts tagged with "{{ title }}"

5 | {% for collectible in collectibles %} 6 | {% include "partials/preview.html" %} 7 | {% endfor %} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /barely/blueprints/default/config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/blueprints/default/config.yaml -------------------------------------------------------------------------------- /barely/blueprints/default/metadata.yaml: -------------------------------------------------------------------------------- 1 | site_url: https://default.com 2 | site_name: Default Website 3 | site_description: Default blueprint description 4 | site_keywords: 5 | - default 6 | - blueprints 7 | favicon: res/favicon.ico 8 | -------------------------------------------------------------------------------- /barely/blueprints/default/res/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/blueprints/default/res/favicon.ico -------------------------------------------------------------------------------- /barely/blueprints/default/res/style.sass: -------------------------------------------------------------------------------- 1 | body 2 | padding: 48px 3 | -------------------------------------------------------------------------------- /barely/blueprints/default/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "partials/base.html" %} 2 | 3 | {% block body %} 4 | {{ content }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /barely/blueprints/default/templates/partials/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ seo_tags }} 5 | 6 | {% block extra_css %}{% endblock %} 7 | 8 | 9 | {% block body %} 10 | 11 | {% endblock %} 12 | 13 | {% block extra_js %}{% endblock %} 14 | 15 | 16 | -------------------------------------------------------------------------------- /barely/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/common/__init__.py -------------------------------------------------------------------------------- /barely/common/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Load the config and return it as a dict 3 | """ 4 | import os 5 | import yaml 6 | import logging 7 | 8 | 9 | class Config: 10 | """ just here to return the config """ 11 | logger = logging.getLogger("base.core") 12 | config = {} 13 | 14 | def __init__(self): 15 | if os.environ.get("barely") is None: 16 | import sys 17 | self.logger.info("something went wrong; was barely started the proper way?") 18 | sys.exit() 19 | 20 | with open(os.path.join(os.path.dirname(__file__), "empty_config.yaml")) as file: 21 | empty_config = file.read() 22 | empty_dict = yaml.safe_load(empty_config) 23 | 24 | with open(os.path.join(os.environ.get("barely"), "config.yaml")) as file: 25 | raw_config = file.read() 26 | config_dict = yaml.safe_load(raw_config) 27 | 28 | config_dict = empty_dict | config_dict 29 | config_dict["PLUGIN_PATHS"] = self.get_plugin_locales() 30 | self.config = config_dict 31 | 32 | @staticmethod 33 | def get_plugin_locales(): 34 | barely_dir = os.path.dirname(os.path.dirname(__file__)) 35 | sysplugin_parent = os.path.join(barely_dir, "plugins") 36 | userplugin_parent = os.path.join(os.environ["barely_appdir"], "plugins") 37 | 38 | return { 39 | "SYS": { 40 | "CONTENT": os.path.join(sysplugin_parent, "content"), 41 | "BACKUP": os.path.join(sysplugin_parent, "backup"), 42 | "PUBLICATION": os.path.join(sysplugin_parent, "publication") 43 | }, 44 | "USER": { 45 | "CONTENT": os.path.join(userplugin_parent, "content"), 46 | "BACKUP": os.path.join(userplugin_parent, "backup"), 47 | "PUBLICATION": os.path.join(userplugin_parent, "publication") 48 | } 49 | } 50 | 51 | def get_config(self): 52 | """ return the config """ 53 | return self.config 54 | 55 | def set_config(self, new_config): 56 | """ override the config """ 57 | self.config = new_config 58 | 59 | 60 | config = Config().get_config() 61 | -------------------------------------------------------------------------------- /barely/common/decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Decorators used in this project 3 | go here. 4 | """ 5 | 6 | 7 | class Singleton: 8 | """ 9 | [source: https://stackoverflow.com/a/7346105] 10 | 11 | A non-thread-safe helper class to ease implementing singletons. 12 | This should be used as a decorator -- not a metaclass -- to the 13 | class that should be a singleton. 14 | 15 | The decorated class can define one `__init__` function that 16 | takes only the `self` argument. Also, the decorated class cannot be 17 | inherited from. Other than that, there are no restrictions that apply 18 | to the decorated class. 19 | 20 | To get the singleton instance, use the `instance` method. Trying 21 | to use `__call__` will result in a `TypeError` being raised. 22 | 23 | """ 24 | 25 | def __init__(self, decorated): 26 | self._decorated = decorated 27 | 28 | def instance(self): 29 | """ 30 | Returns the singleton instance. Upon its first call, it creates a 31 | new instance of the decorated class and calls its `__init__` method. 32 | On all subsequent calls, the already created instance is returned. 33 | 34 | """ 35 | try: 36 | return self._instance 37 | except AttributeError: 38 | self._instance = self._decorated() 39 | return self._instance 40 | 41 | def __call__(self): 42 | raise TypeError('Singletons must be accessed through `instance()`.') 43 | 44 | def __instancecheck__(self, inst): 45 | return isinstance(inst, self._decorated) 46 | -------------------------------------------------------------------------------- /barely/common/empty_config.yaml: -------------------------------------------------------------------------------- 1 | ROOT: 2 | DEV: ~ 3 | WEB: ~ 4 | TEMPLATES_DIR: templates 5 | PAGE_EXT: md 6 | IMAGE_EXT: 7 | - jpg 8 | - jpeg 9 | - png 10 | IGNORE: 11 | - .git 12 | -------------------------------------------------------------------------------- /barely/core/ChangeTracker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Watchdog that can be started / stopped 3 | by the user. Will notify ChangeHandler 4 | of changes to files and dirs in devroot 5 | 6 | Useful for live development 7 | """ 8 | 9 | 10 | import time 11 | import signal 12 | import logging 13 | from livereload import Server 14 | from multiprocessing import Process 15 | from watchdog.observers import Observer 16 | from watchdog.events import PatternMatchingEventHandler 17 | from barely.common.config import config 18 | 19 | 20 | def empty_func(): 21 | return None 22 | 23 | 24 | class ChangeTracker: 25 | """ monitors the devroot for file and dir changes and notifies the ChangeHandler """ 26 | logger = logging.getLogger("base.core") 27 | logger_indented = logging.getLogger("indented") 28 | 29 | def __init__(self, EH=None): 30 | self.logger.debug("new ChangeTracker created") 31 | if EH is not None: 32 | self.register_handler(EH) 33 | else: 34 | self.handler_available = False 35 | self.eventbuffer = [] 36 | self.verbose = False 37 | 38 | def register_handler(self, EH): 39 | """ register an event handler. The EH gets notified about observed changes """ 40 | patterns = "*" 41 | ignore_patterns = "" 42 | ignore_directories = False 43 | case_sensitive = True 44 | handler = PatternMatchingEventHandler(patterns=patterns, ignore_patterns=ignore_patterns, ignore_directories=ignore_directories, case_sensitive=case_sensitive) 45 | 46 | handler.on_created = self.buffer 47 | handler.on_deleted = self.buffer 48 | handler.on_modified = self.buffer 49 | handler.on_moved = self.buffer 50 | self.EH = EH 51 | self.logger.debug("registered and configured a change-handler") 52 | 53 | """ set up the Observer """ 54 | recursive = True 55 | self.observer = Observer() 56 | self.observer.schedule(handler, config["ROOT"]["DEV"], recursive=recursive) 57 | self.logger.debug("configured an observer") 58 | 59 | self.handler_available = True 60 | 61 | def track(self, loop_action=empty_func): 62 | """ start the watchdog configured above """ 63 | if self.handler_available: 64 | # setup the livereload server 65 | server = Server() 66 | if not self.verbose: 67 | server._setup_logging = empty_func 68 | server.watch(config["ROOT"]["WEB"], delay=0.1) 69 | self.liveserver = Process(target=server.serve, kwargs={"root": config["ROOT"]["WEB"], "open_url_delay": 1}) 70 | 71 | self.liveserver.start() 72 | self.observer.start() 73 | self.tracking = True 74 | self.logger.debug("liveserver and observer started") 75 | self.logger.info("started tracking...") 76 | 77 | # handle SIGINTs. Store the original to later reuse it. 78 | self.original_sigint = signal.getsignal(signal.SIGINT) 79 | signal.signal(signal.SIGINT, self.stop) 80 | 81 | # check buffer every 0.2s; should be enough to catch duplicate events 82 | while self.tracking: 83 | time.sleep(0.2) 84 | loop_action() 85 | self.empty_buffer() 86 | else: 87 | raise Exception("No available handler. Not tracking.") 88 | 89 | def buffer(self, event): 90 | # spares the hassle of dealing with different types of events. 91 | event.relevant_path = event.dest_path if hasattr(event, "dest_path") else event.src_path 92 | self.logger.debug(f"buffered event at {event.relevant_path}") 93 | i = 0 94 | while i < len(self.eventbuffer): 95 | older = self.eventbuffer[i] 96 | if type(event) is type(older) and event.relevant_path == older.relevant_path: 97 | self.eventbuffer.pop(i) 98 | self.logger.debug("removed duplicate older event") 99 | i += 1 100 | self.eventbuffer.append(event) 101 | 102 | def empty_buffer(self): 103 | for event in self.eventbuffer: 104 | self.EH.notify(event) 105 | self.eventbuffer = [] 106 | 107 | def stop(self, signum, frame): 108 | self.logger.debug("received signal to stop tracking") 109 | signal.signal(signal.SIGINT, self.original_sigint) 110 | 111 | self.tracking = False 112 | self.observer.stop() 113 | self.observer.join() 114 | self.liveserver.join() 115 | 116 | print() 117 | print("\033[A\033[A") 118 | self.logger.info("stopped tracking.") 119 | -------------------------------------------------------------------------------- /barely/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/core/__init__.py -------------------------------------------------------------------------------- /barely/plugins/PluginManager.py: -------------------------------------------------------------------------------- 1 | """ 2 | During barelys startup, PluginManager 3 | finds and registers all available 4 | Plugins. This includes barelys core plugins, 5 | as well as any the user might have put in their 6 | dotfolder. 7 | Plugins are being differntiated by their category 8 | (content, backup or publication) and by the 9 | filetype(s) they register for. 10 | """ 11 | 12 | import os 13 | import sys 14 | import logging 15 | from inspect import isclass 16 | from pkgutil import iter_modules 17 | from importlib import import_module 18 | from abc import ABC, abstractmethod 19 | from collections.abc import Iterable 20 | from barely.common.config import config 21 | 22 | 23 | class PluginBase(ABC): 24 | """ base class for all plugins """ 25 | 26 | def __init__(self, *args, **kwargs): 27 | self.config = config 28 | self.logger = logging.getLogger("base.plugin") 29 | self.logger_indented = logging.getLogger("indented") 30 | 31 | @abstractmethod 32 | def register(self): 33 | return "Base", -1, [] 34 | 35 | @abstractmethod 36 | def action(self, item, *args, **kwargs): 37 | yield item 38 | 39 | def finalize(self): 40 | pass 41 | 42 | 43 | class PluginManager: 44 | """ finds, registers and pipes in plugins """ 45 | logger = logging.getLogger("base.core") 46 | 47 | plugin_count = 0 48 | 49 | def __init__(self): 50 | self.logger.info("registering plugins...") 51 | self.plugins_content = self.discover_plugins([config["PLUGIN_PATHS"]["SYS"]["CONTENT"], config["PLUGIN_PATHS"]["USER"]["CONTENT"]]) 52 | self.plugins_backup = self.discover_plugins([config["PLUGIN_PATHS"]["SYS"]["BACKUP"], config["PLUGIN_PATHS"]["USER"]["BACKUP"]], type_content=False) 53 | self.plugins_publication = self.discover_plugins([config["PLUGIN_PATHS"]["SYS"]["PUBLICATION"], 54 | config["PLUGIN_PATHS"]["USER"]["PUBLICATION"]], type_content=False) 55 | self.logger.info(f"{self.plugin_count} plugins registered.") 56 | 57 | def discover_plugins(self, paths, type_content=True): 58 | """ checks the path for plugin files, then imports them """ 59 | 60 | module_paths = paths.copy() 61 | for path in paths: 62 | self.logger.debug(f"checking for plugins in {path}") 63 | if os.path.exists(path): 64 | subdirs = (next(os.walk(path))[1]) # get all first level subdirs 65 | module_paths.extend([os.path.join(path, sub) for sub in subdirs]) # necessary, otherwise wrong relative paths 66 | sys.path.extend(module_paths) # necessary for python to import from here 67 | 68 | registered = [] # used to ensure no duplicate plugins will be registered 69 | 70 | found_plugins = {} if type_content else [] 71 | for (_, module_name, _) in iter_modules(module_paths): 72 | try: 73 | module = import_module(f"{module_name}") 74 | except Exception: 75 | continue 76 | 77 | for attribute_name in dir(module): 78 | attribute = getattr(module, attribute_name) 79 | 80 | # necessary to filter out the imported parent class 81 | if isclass(attribute) and issubclass(attribute, PluginBase) and not issubclass(PluginBase, attribute): 82 | # is that a long-ass try-except? yes. but if a plugin is broken, idk about knowing why, I just do not want to initialize it. 83 | 84 | try: 85 | plugin_instance = attribute() 86 | if type_content: 87 | name, priority, registered_for = plugin_instance.register() 88 | if name in registered: 89 | continue 90 | registered.append(name) 91 | self.logger.debug(f"found the content plugin {name} of priority {priority}") 92 | priority = int(priority) 93 | if priority > -1: 94 | self.plugin_count += 1 95 | for extension in registered_for: 96 | found_plugins.setdefault(extension, []).append((plugin_instance, priority)) 97 | else: 98 | name, priority = plugin_instance.register() 99 | if name in registered: 100 | continue 101 | registered.append(name) 102 | self.logger.debug(f"found the plugin {name} of priority {priority}") 103 | if priority > -1: 104 | self.plugin_count += 1 105 | found_plugins.append((plugin_instance, priority)) 106 | except Exception: 107 | pass 108 | 109 | sys.path = list(set(sys.path) - set(module_paths)) # remove our added entries to path 110 | 111 | if type_content: 112 | for ext, registered in found_plugins.items(): 113 | registered.sort(key=lambda t: t[1]) 114 | found_plugins[ext] = [r[0] for r in registered] 115 | else: 116 | found_plugins.sort(key=lambda t: t[1]) 117 | found_plugins = [r[0] for r in found_plugins] 118 | 119 | return found_plugins 120 | 121 | def hook_content(self, to_hook): 122 | item_list = [to_hook] 123 | for plugin in self.plugins_content.get(to_hook["extension"], []): 124 | returned_items = [] 125 | for i in item_list: 126 | returned = plugin.action(item=i) 127 | if isinstance(returned, Iterable) and not isinstance(returned, str): 128 | for r in returned: 129 | returned_items.append(r) 130 | else: 131 | returned_items.append(returned) 132 | item_list = returned_items 133 | return item_list 134 | 135 | def finalize_content(self): 136 | plugins = set() 137 | for ext in self.plugins_content: 138 | for plugin in self.plugins_content[ext]: 139 | plugins.add(plugin) 140 | 141 | for plugin in plugins: 142 | name, _, _ = plugin.register() 143 | self.logger.info(f"Finalizing plugin {name}...") 144 | plugin.finalize() 145 | 146 | def hook_backup(self): 147 | for plugin in self.plugins_backup: 148 | plugin.action() 149 | 150 | def hook_publication(self): 151 | for plugin in self.plugins_publication: 152 | plugin.action() 153 | -------------------------------------------------------------------------------- /barely/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Since PluginBase is to be extended 3 | by third parties, make it just a 4 | tad bit easier to import. 5 | """ 6 | 7 | from barely.plugins.PluginManager import PluginBase as PB 8 | 9 | PluginBase = PB 10 | -------------------------------------------------------------------------------- /barely/plugins/backup/LocalBackup/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/backup/LocalBackup/__init__.py -------------------------------------------------------------------------------- /barely/plugins/backup/LocalBackup/localbackup.py: -------------------------------------------------------------------------------- 1 | """ 2 | BackupManager. Requires from config: 3 | - BACKUPS MAX 4 | 5 | Provides methods to create backups 6 | and to restore them. 7 | """ 8 | 9 | import os 10 | import glob 11 | import shutil 12 | from datetime import datetime 13 | from barely.plugins import PluginBase 14 | 15 | 16 | class LocalBackup(PluginBase): 17 | # save copies of the devroot in a local folder. very rudimentary backup strategy, but better than nothing, I guess. 18 | 19 | def __init__(self): 20 | super().__init__() 21 | try: 22 | standard_config = { 23 | "PRIORITY": 30, 24 | "MAX": 10, 25 | "BAKROOT": os.path.join(os.path.dirname(self.config["ROOT"]["DEV"]), "backups") 26 | } 27 | self.plugin_config = standard_config | self.config["LOCAL_BACKUP"] 28 | except KeyError: 29 | self.plugin_config = {"PRIORITY": -1} 30 | 31 | def register(self): 32 | return "LocalBackup", self.plugin_config["PRIORITY"] 33 | 34 | def action(self, *args, **kwargs): 35 | backup_name = "BACKUP--" + datetime.now().strftime("%Y-%m-%d--%H-%M-%S") 36 | backup_path = os.path.join(self.plugin_config["BAKROOT"], backup_name) 37 | shutil.copytree(self.config["ROOT"]["DEV"], backup_path, symlinks=False) 38 | 39 | existing = sorted(glob.glob(os.path.join(self.plugin_config["BAKROOT"], "BACKUP--*"), reverse=True)) 40 | while len(existing) >= self.plugin_config["MAX"]: 41 | shutil.rmtree(existing.pop(-1)) 42 | 43 | # we don't want to backup any existing git stuff 44 | try: 45 | shutil.rmtree(os.path.join(backup_path, ".git")) 46 | except FileNotFoundError: 47 | pass 48 | 49 | self.logger.info(f"created backup: {backup_name}") 50 | -------------------------------------------------------------------------------- /barely/plugins/backup/LocalBackup/test_localbackup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from mock import patch, call 4 | from unittest.mock import MagicMock 5 | from barely.plugins.backup.LocalBackup.localbackup import LocalBackup 6 | 7 | 8 | class TestLocalBackup(unittest.TestCase): 9 | 10 | def test___init__(self): 11 | lb = LocalBackup() 12 | self.assertDictEqual({"PRIORITY": -1}, lb.plugin_config) 13 | 14 | lb.config["LOCAL_BACKUP"] = {"PRIORITY": 2} 15 | lb.__init__() 16 | 17 | golden = { 18 | "PRIORITY": 2, 19 | "MAX": 10, 20 | "BAKROOT": os.path.join(os.path.dirname(lb.config["ROOT"]["DEV"]), "backups") 21 | } 22 | 23 | self.assertDictEqual(golden, lb.plugin_config) 24 | 25 | # reset 26 | del lb.config["LOCAL_BACKUP"] 27 | lb.__init__() 28 | 29 | def test_register(self): 30 | lb = LocalBackup() 31 | name, prio = lb.register() 32 | 33 | self.assertEqual(name, "LocalBackup") 34 | self.assertEqual(prio, -1) 35 | 36 | @patch("barely.plugins.backup.LocalBackup.localbackup.glob") 37 | @patch("barely.plugins.backup.LocalBackup.localbackup.shutil") 38 | def test_action(self, shutil, glob): 39 | shutil.copytree = MagicMock() 40 | shutil.rmtree = MagicMock() 41 | 42 | glob.glob = MagicMock(return_value=[]) 43 | 44 | lb = LocalBackup() 45 | lb.plugin_config["MAX"] = 2 46 | lb.plugin_config["BAKROOT"] = "backups" 47 | lb.action() 48 | 49 | self.assertTrue(shutil.copytree.called) 50 | self.assertTrue(shutil.rmtree.called_once) 51 | self.assertTrue(glob.glob.called) 52 | 53 | glob.glob.return_value = [1, 2, 3] 54 | shutil.rmtree.reset_mock() 55 | lb.action() 56 | self.assertTrue(call(3) in shutil.rmtree.call_args_list) 57 | self.assertTrue(call(2) in shutil.rmtree.call_args_list) 58 | 59 | lb.plugin_config["MAX"] = 10 60 | -------------------------------------------------------------------------------- /barely/plugins/backup/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/backup/__init__.py -------------------------------------------------------------------------------- /barely/plugins/backup/git/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/backup/git/__init__.py -------------------------------------------------------------------------------- /barely/plugins/backup/git/git.py: -------------------------------------------------------------------------------- 1 | """ 2 | push to git remote rep automatically 3 | """ 4 | import os 5 | os.environ["GIT_PYTHON_REFRESH"] = "quiet" 6 | 7 | from git import Repo 8 | from datetime import datetime 9 | from barely.plugins import PluginBase 10 | 11 | 12 | class Git(PluginBase): 13 | # add changes / commit them / push them to origin 14 | 15 | def __init__(self): 16 | super().__init__() 17 | standard_config = { 18 | "PRIORITY": 40, 19 | "MESSAGE": "barely auto commit", 20 | "REMOTE_NAME": "origin" 21 | } 22 | try: 23 | self.plugin_config = standard_config | self.config["GIT"] 24 | except KeyError: 25 | self.plugin_config = standard_config 26 | 27 | def register(self): 28 | return "git", self.plugin_config["PRIORITY"] 29 | 30 | def action(self, *args, **kwargs): 31 | try: 32 | repo = Repo(os.path.join(self.config["ROOT"]["DEV"], ".git")) 33 | repo.git.add(all=True) 34 | repo.index.commit(datetime.now().strftime("%Y-%m-%d--%H-%M-%S") + " " + self.plugin_config["MESSAGE"]) 35 | origin = repo.remote(name=self.plugin_config["REMOTE_NAME"]) 36 | origin.push() 37 | self.logger.info(f"successfully pushed to {self.plugin_config['REMOTE_NAME']}") 38 | except Exception: 39 | self.logger.error(f"an error occurred while pushing to {self.plugin_config['REMOTE_NAME']}") 40 | -------------------------------------------------------------------------------- /barely/plugins/backup/git/test_git.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from mock import patch 3 | from unittest.mock import MagicMock 4 | from barely.plugins.backup.git.git import Git 5 | 6 | 7 | class TestGit(unittest.TestCase): 8 | 9 | def test___init__(self): 10 | git = Git() 11 | 12 | golden = { 13 | "PRIORITY": 40, 14 | "MESSAGE": "barely auto commit", 15 | "REMOTE_NAME": "origin" 16 | } 17 | 18 | self.assertDictEqual(golden, git.plugin_config) 19 | 20 | def test_register(self): 21 | git = Git() 22 | name, prio = git.register() 23 | 24 | self.assertEqual(name, "git") 25 | self.assertEqual(prio, 40) 26 | 27 | @patch("barely.plugins.backup.git.git.Repo") 28 | def test_action(self, r): 29 | repo = MagicMock() 30 | 31 | repo.git.add = MagicMock() 32 | repo.index.commit = MagicMock() 33 | 34 | repo.remote = MagicMock() 35 | 36 | r.return_value = repo 37 | 38 | git = Git() 39 | git.plugin_config = { 40 | "PRIORITY": 2, 41 | "MESSAGE": "barely auto commit", 42 | "REMOTE_NAME": "origin" 43 | } 44 | git.action() 45 | 46 | repo.git.add.assert_called() 47 | repo.index.commit.assert_called() 48 | 49 | repo.remote.assert_called() 50 | 51 | def error(): 52 | raise Exception 53 | 54 | repo.git.add = MagicMock(side_effect=error) 55 | 56 | with self.assertRaises(Exception) as context: 57 | git.action() 58 | self.assertTrue('barely :: an error occurred while pushing to origin' in str(context.exception)) 59 | -------------------------------------------------------------------------------- /barely/plugins/content/AutoSEO/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/AutoSEO/__init__.py -------------------------------------------------------------------------------- /barely/plugins/content/AutoSEO/autoseo.py: -------------------------------------------------------------------------------- 1 | """ 2 | auto-generate lots of SEO-relevant tags 3 | 4 | ...because doing it manually is boring. 5 | """ 6 | import os 7 | import re 8 | import glob 9 | from barely.plugins import PluginBase 10 | 11 | 12 | class AutoSEO(PluginBase): 13 | 14 | def __init__(self): 15 | super().__init__() 16 | standard_config = { 17 | "PRIORITY": 30, 18 | "MISC_TAGS": True 19 | } 20 | try: 21 | self.plugin_config = standard_config | self.config["AUTO_SEO"] 22 | except KeyError: 23 | self.plugin_config = standard_config 24 | self.url = "" 25 | 26 | def register(self): 27 | return "AutoSEO", self.plugin_config["PRIORITY"], [self.config["PAGE_EXT"]] 28 | 29 | def action(self, item): 30 | if "parent_meta" not in item: 31 | # 1. extract as much information as possible from the page item 32 | seo = self._extract_tags(item) 33 | 34 | # 2. do some post-processing & filling in of gaps 35 | 36 | # append the site_title to the title, should they exist; 37 | # set the site_title as title, should the latter not exist 38 | if "title" in seo and "site_title" in seo: 39 | seo["title"] = f"{seo['title']} | {seo['site_title']}" 40 | if "title" not in seo and "site_title" in seo: 41 | seo["title"] = seo["site_title"] 42 | 43 | # if no image was specified, find one now 44 | if "og:image" not in seo: 45 | try: 46 | seo["og:image"] = re.findall(r"]+?src\s*=\s*['\"]?([^\s'\"?#>]+)", item["content"])[0] 47 | except IndexError: 48 | seo |= self._first_image(os.path.dirname(item["destination"])) 49 | 50 | # find the absolute URL of the image, and 51 | # compute the og:url from site_url and destination of item 52 | if "og:url" in seo: 53 | # keep up to date for finalize 54 | self.url = seo["og:url"].rstrip("/") 55 | page_path = item["destination"].replace(self.config["ROOT"]["WEB"], "").replace("\\", "/") 56 | 57 | if "og:image" in seo and os.path.isabs(seo["og:image"]): 58 | seo["og:image"] = self.url + seo["og:image"] 59 | elif "og:image" in seo and not seo["og:image"].startswith("http"): 60 | img_path = os.path.dirname(page_path) + "/" + seo["og:image"] 61 | seo["og:image"] = self.url + img_path 62 | 63 | seo["og:url"] = self.url + page_path 64 | 65 | # 3. generate the meta html tags 66 | item["meta"]["seo_tags"] = self._generate_tags(seo) 67 | 68 | yield item 69 | 70 | def finalize(self): 71 | if not self.url: 72 | # not enough information to continue 73 | return 74 | 75 | # sitemap.txt 76 | sitemap_dev = os.path.join(self.config["ROOT"]["DEV"], "sitemap.txt") 77 | sitemap_web = os.path.join(self.config["ROOT"]["WEB"], "sitemap.txt") 78 | sitemap_url = self.url + "/" + "sitemap.txt" 79 | 80 | if not os.path.exists(sitemap_dev): 81 | pages = glob.glob(os.path.join(self.config["ROOT"]["WEB"], "**", "*.html"), recursive=True) 82 | pages = [f.replace(self.config["ROOT"]["WEB"], self.url).replace("\\", "/") for f in pages] 83 | pages = "\n".join(pages) 84 | 85 | with open(sitemap_web, "w") as file: 86 | file.write(pages) 87 | 88 | # robots.txt 89 | robots_dev = os.path.join(self.config["ROOT"]["DEV"], "robots.txt") 90 | robots_web = os.path.join(self.config["ROOT"]["WEB"], "robots.txt") 91 | 92 | if not os.path.exists(robots_dev): 93 | robots = f"User-agent: *\nAllow: /\n\nSitemap: {sitemap_url}" 94 | 95 | with open(robots_web, "w") as file: 96 | file.write(robots) 97 | 98 | @staticmethod 99 | def _extract_tags(item): 100 | try: 101 | page_seo = item["meta"]["SEO"] 102 | except KeyError: 103 | page_seo = {} 104 | 105 | # Page-specific metadata; already merged with global metadata in ProcessingPipeline! 106 | def get_page(tag, rebrand=None): 107 | rebrand = rebrand if rebrand else tag 108 | if tag in item["meta"]: 109 | return {rebrand: item["meta"][tag]} 110 | return {} 111 | 112 | # Page & SEO specific metadata, can differ from page-specific one 113 | def get_seo(tag, rebrand=None): 114 | rebrand = rebrand if rebrand else tag 115 | if tag in page_seo: 116 | return {rebrand: page_seo[tag]} 117 | return {} 118 | 119 | seo = {} 120 | 121 | # extract all the necessary info, from wherever we can get it 122 | # rightmost is preferred 123 | 124 | # title 125 | seo |= get_page("title") 126 | # description 127 | seo |= get_page("site_description", "description") | get_page("summary", "description") | get_page("description") 128 | # robots 129 | seo |= {"robots": "all"} | get_page("robots") 130 | # keywords 131 | keywords = item["meta"]["keywords"] if "keywords" in item["meta"] else [] 132 | site_keywords = item["meta"]["site_keywords"] if "site_keywords" in item["meta"] else [] 133 | seo |= {"keywords": ", ".join(list(set(site_keywords) | set(keywords)))} 134 | # favicon 135 | seo |= get_page("favicon") 136 | 137 | # og:title 138 | seo |= get_page("title", "og:title") | get_seo("title", "og:title") 139 | # og:description 140 | seo |= get_page("site_description", "og:description") | get_page("summary", "og:description") | get_page("description", "og:description") | get_seo("description", "og:description") 141 | # og:image 142 | seo |= get_page("title_image", "og:image") | get_seo("title_image", "og:image") 143 | # og:url 144 | seo |= get_page("site_url", "og:url") 145 | # og:site_name 146 | seo |= get_page("title", "og:site_name") | get_page("site_name", "og:site_name") | get_seo("site_name", "og:site_name") 147 | 148 | # twitter:image:alt 149 | seo |= get_page("site_description", "twitter:image:alt") | get_page("summary", "twitter:image:alt") | get_page("description", "twitter:image:alt") | get_seo("description", "twitter:image:alt") | get_page("title_image_alt", "twitter:image:alt") | get_seo("title_image_alt", "twitter:image:alt") 150 | # twitter:card 151 | seo |= {"twitter:card": "summary_card_large"} | get_seo("twitter_card", "twitter:card") 152 | # twitter:site 153 | seo |= get_page("twitter_site", "twitter:site") | get_seo("twitter_site", "twitter:site") 154 | # twitter:creator 155 | seo |= get_page("twitter_creator", "twitter:creator") | get_seo("twitter_creator", "twitter:creator") 156 | 157 | return seo 158 | 159 | def _generate_tags(self, seo): 160 | tags = [] 161 | 162 | if self.plugin_config["MISC_TAGS"]: 163 | tags.append('') 164 | tags.append('') 165 | 166 | if "title" in seo and "og:site_name" in seo: 167 | tags.append(f'{seo["title"]} | {seo["og:site_name"]}') 168 | elif "title" in seo: 169 | tags.append(f'{seo["title"]}') 170 | elif "og:site_name" in seo: 171 | tags.append(f'{seo["og:site_name"]}') 172 | if "description" in seo: 173 | tags.append(f'') 174 | if "keywords" in seo: 175 | tags.append(f'') 176 | if "robots" in seo: 177 | tags.append(f'') 178 | if "favicon" in seo: 179 | tags.append(f'') 180 | 181 | if "og:title" in seo: 182 | tags.append(f'') 183 | if "og:description" in seo: 184 | tags.append(f'') 185 | if "og:image" in seo: 186 | tags.append(f'') 187 | if "og:url" in seo: 188 | tags.append(f'') 189 | if "og:site_name" in seo: 190 | tags.append(f'') 191 | 192 | if "twitter:image:alt" in seo: 193 | tags.append(f'') 194 | if "twitter:site" in seo: 195 | tags.append(f'') 196 | if "twitter:creator" in seo: 197 | tags.append(f'') 198 | if "twitter:card" in seo: 199 | tags.append(f'') 200 | 201 | return "\n".join(tags) 202 | 203 | def _first_image(self, path): 204 | images = [] 205 | types = [os.path.join(path, f"*.{t}") for t in self.config["IMAGE_EXT"]] 206 | for files in types: 207 | images.extend(glob.glob(files)) 208 | 209 | if len(images): 210 | return {"og:image": images[0].replace(self.config["ROOT"]["WEB"], "").replace("\\", "/")} 211 | return {} 212 | -------------------------------------------------------------------------------- /barely/plugins/content/AutoSEO/test_autoseo.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import unittest 4 | from barely.plugins.content.AutoSEO.autoseo import AutoSEO 5 | 6 | 7 | class TestAutoSEO(unittest.TestCase): 8 | 9 | maxDiff = None 10 | 11 | def test_action(self): 12 | global_meta = { 13 | "site_url": "https://test.com/", 14 | "site_name": "Test Site", 15 | "site_description": "Description of test site", 16 | "site_keywords": ["one"], 17 | "favicon": "favicon.ico" 18 | } 19 | 20 | page_meta = { 21 | "title": "Test Page", 22 | "summary": "just a simple page for testing", 23 | "SEO": { 24 | "description": "AMAZING TEST PAGE!!" 25 | } 26 | } 27 | 28 | content = """ 29 |

Test Image One

30 |

Test Image Two

31 | """ 32 | 33 | item = { 34 | "content": content, 35 | "meta": global_meta | page_meta, 36 | "destination": "web/subpage/index.html" 37 | } 38 | 39 | golden_tags = """ 40 | 41 | 42 | Test Page | Test Site 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | """ 55 | 56 | aseo = AutoSEO() 57 | result = list(aseo.action(item=item))[0]["meta"]["seo_tags"] 58 | self.assertEqual(re.sub(r'[\s+]', '', result), re.sub(r'[\s+]', '', golden_tags)) 59 | 60 | def test__first_image(self): 61 | aseo = AutoSEO() 62 | result = aseo._first_image(os.path.join("Plugins", "AutoSEO")) 63 | self.assertDictEqual(result, { 64 | "og:image": "Plugins/AutoSEO/b.jpg" 65 | }) 66 | -------------------------------------------------------------------------------- /barely/plugins/content/Collections/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/Collections/__init__.py -------------------------------------------------------------------------------- /barely/plugins/content/Forms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/Forms/__init__.py -------------------------------------------------------------------------------- /barely/plugins/content/Forms/forms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Generate forms from yaml shorthand. 3 | Allow the easy creation of forms 4 | by requiring the smallest possible 5 | amount of information to generate 6 | any HTML5 form. 7 | """ 8 | import re 9 | from barely.plugins import PluginBase 10 | 11 | 12 | class Forms(PluginBase): 13 | # generate an arbitrary amount of forms for any page, specified through yaml 14 | 15 | form_defaults = { 16 | "classes": "", 17 | "action": "" 18 | } 19 | 20 | group_defaults = { 21 | "classes": "", 22 | "legend": "" 23 | } 24 | 25 | field_defaults = { 26 | "required": False, 27 | "label": False, 28 | "label-after": False, 29 | "type": "", 30 | "options": {}, 31 | "default": "", 32 | "multiple": False, 33 | "classes": "", 34 | "placeholder": "", 35 | "value": "", 36 | "text": "", 37 | "rows": "", 38 | "cols": "" 39 | } 40 | 41 | def __init__(self): 42 | super().__init__() 43 | standard_config = { 44 | "PRIORITY": 5 45 | } 46 | try: 47 | self.plugin_config = standard_config | self.config["FORMS"] 48 | except KeyError: 49 | self.plugin_config = standard_config 50 | 51 | def register(self): 52 | return "Forms", self.plugin_config["PRIORITY"], [self.config["PAGE_EXT"]] 53 | 54 | def action(self, *args, **kwargs): 55 | if "item" in kwargs: 56 | item = kwargs["item"] 57 | try: 58 | form_list = item["meta"]["forms"] 59 | except KeyError: 60 | yield item 61 | return 62 | 63 | for key, value in form_list.items(): 64 | # try: 65 | item["meta"]["form_" + key] = "\n".join(self.generate_form(key, **value)) 66 | # except Exception: 67 | # pass 68 | yield item 69 | 70 | def generate_form(self, form_name, **form_config): 71 | form_config = self.form_defaults | form_config 72 | yield f"
" 73 | for field, settings in form_config.items(): 74 | if field in ["action", "classes"]: 75 | continue 76 | yield from self.render_fields(form_name, field, **settings) 77 | yield "
" 78 | 79 | def render_fields(self, form_name, field_name, **field_config): 80 | id = "form-" + form_name + "-" + field_name 81 | 82 | if re.match(r"^group-.+", field_name): 83 | field_config = self.group_defaults | field_config 84 | yield f"
" 85 | 86 | if field_config["legend"]: 87 | yield f"{field_config['legend']}" 88 | 89 | for field, settings in field_config.items(): 90 | if field in ["legend", "classes"]: 91 | continue 92 | yield from self.render_fields(form_name, field, **settings) 93 | 94 | yield "
" 95 | 96 | else: 97 | field_config = self.field_defaults | field_config 98 | required = "required" if field_config["required"] else "" 99 | if field_config["label"]: 100 | yield f"" 101 | 102 | if field_config["type"] == "radio": 103 | for value, option in field_config["options"].items(): 104 | checked = "checked" if value == field_config["default"] else "" 105 | radio_id = f"{id}-{value}" 106 | yield f"" 107 | yield f"" 108 | 109 | elif field_config["type"] == "select": 110 | multiple = "multiple" if field_config["multiple"] else "" 111 | yield f"" 117 | 118 | elif field_config["type"] == "button": 119 | yield (f"") 121 | 122 | elif field_config["type"] == "textarea": 123 | yield (f"") 125 | 126 | elif field_config["type"] == "status": 127 | yield (f"
") 128 | else: 129 | yield (f"") 132 | 133 | if field_config["label-after"]: 134 | yield f"" 135 | -------------------------------------------------------------------------------- /barely/plugins/content/Forms/test_forms.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | from barely.plugins.content.Forms.forms import Forms 4 | 5 | 6 | class TestForms(unittest.TestCase): 7 | 8 | def test___init__(self): 9 | f = Forms() 10 | self.assertDictEqual({"PRIORITY": 5}, f.plugin_config) 11 | 12 | golden = { 13 | "PRIORITY": 2 14 | } 15 | f.config["FORMS"] = {"PRIORITY": 2} 16 | f.__init__() 17 | 18 | self.assertDictEqual(golden, f.plugin_config) 19 | 20 | # reset 21 | del f.config["FORMS"] 22 | f.__init__() 23 | 24 | def test_register(self): 25 | f = Forms() 26 | name, prio, ext = f.register() 27 | 28 | self.assertEqual(name, "Forms") 29 | self.assertEqual(prio, 5) 30 | self.assertEqual(ext, ["md"]) 31 | 32 | def test_action(self): 33 | item = { 34 | "meta": { 35 | "forms": { 36 | "sample": { 37 | "action": "test.php", 38 | "classes": "large", 39 | "group-basic": { 40 | "legend": "Basic Data", 41 | "name": { 42 | "type": "text", 43 | "required": True, 44 | "placeholder": "Name" 45 | }, 46 | "mmail": { 47 | "type": "email", 48 | "required": True, 49 | "placeholder": "E-Mail" 50 | } 51 | }, 52 | "topic": { 53 | "type": "select", 54 | "multiple": False, 55 | "default": "standard", 56 | "options": { 57 | "n1": "something", 58 | "n2": "standard", 59 | "n3": "something else" 60 | } 61 | }, 62 | "group-radios": { 63 | "rating": { 64 | "type": "radio", 65 | "label": "Please rate us!", 66 | "options": { 67 | 1: "1", 68 | 2: "2", 69 | 3: "3" 70 | } 71 | } 72 | }, 73 | "message": { 74 | "type": "textarea", 75 | "required": True, 76 | "classes": "extra", 77 | "value": "Pre-filled" 78 | }, 79 | "grpd": { 80 | "type": "checkbox", 81 | "value": "agreed", 82 | "label": "Agree to GDPR", 83 | "name": "gdpr-checkbox" 84 | }, 85 | "send": { 86 | "type": "button", 87 | "value": "Send Now", 88 | "action": "submit" 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | golden_form = """ 96 |
97 |
98 | Basic Data 99 | 100 | 101 |
102 | 107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
116 | 117 | 118 | 119 | 120 |
121 | """ 122 | 123 | f = Forms() 124 | result = list(f.action(item=item))[0] 125 | self.assertEqual(re.sub(r'[\s+]', '', golden_form), re.sub(r'[\s+]', '', result["meta"]["form_sample"])) 126 | -------------------------------------------------------------------------------- /barely/plugins/content/Gallery/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/Gallery/__init__.py -------------------------------------------------------------------------------- /barely/plugins/content/Gallery/gallery.py: -------------------------------------------------------------------------------- 1 | """ 2 | Gallery converts a []!!() tag into a 3 | list of globbed images from a specified dir 4 | """ 5 | import re 6 | import os 7 | import glob 8 | from barely.plugins import PluginBase 9 | 10 | 11 | class Gallery(PluginBase): 12 | # Turn a markdown tag into a gallery 13 | 14 | def __init__(self): 15 | super().__init__() 16 | standard_config = { 17 | "PRIORITY": 2, 18 | "DEFAULT_SORT": "name", 19 | "DEFAULT_DIRECTION": "asc", 20 | "GALLERY_CLASS": "gallery" 21 | } 22 | try: 23 | self.plugin_config = standard_config | self.config["GALLERY"] 24 | except KeyError: 25 | self.plugin_config = standard_config 26 | 27 | def register(self): 28 | return "Gallery", self.plugin_config["PRIORITY"], [self.config["PAGE_EXT"]] 29 | 30 | def action(self, *args, **kwargs): 31 | if "item" in kwargs: 32 | item = kwargs["item"] 33 | item["content"] = re.sub(r"\[(?P\S+)\s?(?P\S+)?\s?(?P\S+)?\]!!\((?P\S+)\)", self._handle_matches, item["content"]) 34 | yield item 35 | 36 | def _handle_matches(self, match): 37 | # get info from the gallery tag 38 | try: 39 | sort = match.group("sort") 40 | except KeyError: 41 | sort = self.plugin_config["DEFAULT_SORT"] 42 | 43 | try: 44 | direction = match.group("direction") 45 | except KeyError: 46 | direction = self.plugin_config["DEFAULT_DIRECTION"] 47 | 48 | name = match.group("name") 49 | folder = match.group("folder") 50 | path = folder 51 | 52 | self.logger.info(f"Generating gallery \"{name}\"...") 53 | 54 | if os.path.isabs(path): 55 | path = os.path.join(self.config["ROOT"]["DEV"], path.replace(self.config["ROOT"]["DEV"], "")[1:]) 56 | else: 57 | path = os.path.join(self.config["ROOT"]["DEV"], path) 58 | 59 | # glob all images 60 | images = [] 61 | types = [os.path.join(path, f"*.{t}") for t in self.config["IMAGE_EXT"]] 62 | for files in types: 63 | images.extend(glob.glob(files)) 64 | 65 | # sort them 66 | if "time" in sort or "date" in sort: 67 | images = sorted(images, key=os.path.getmtime) 68 | else: 69 | images = sorted(images) 70 | 71 | # change direction if necessary 72 | if "desc" in direction: 73 | images.reverse() 74 | 75 | # build the gallery 76 | gallery = [] 77 | gallery.append(f"
") 78 | for i in range(len(images)): 79 | image_path = images[i][images[i].index(folder):].replace('\\', '/') 80 | gallery.append(f"\"image") 81 | gallery.append("
") 82 | 83 | return "\n".join(gallery) 84 | -------------------------------------------------------------------------------- /barely/plugins/content/Gallery/test_gallery.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import unittest 4 | from barely.plugins.content.Gallery.gallery import Gallery 5 | 6 | 7 | class TestGallery(unittest.TestCase): 8 | 9 | def test_action(self): 10 | img_path = os.path.join("Plugins", "Gallery") 11 | item = { 12 | "content": f"[examplegallery name desc]!!({img_path})" 13 | } 14 | 15 | golden = """ 16 | 21 | """ 22 | 23 | g = Gallery() 24 | result = list(g.action(item=item))[0]["content"] 25 | self.assertEqual(re.sub(r'[\s+]', '', result), re.sub(r'[\s+]', '', golden)) 26 | -------------------------------------------------------------------------------- /barely/plugins/content/Highlight/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/Highlight/__init__.py -------------------------------------------------------------------------------- /barely/plugins/content/Highlight/highlight.py: -------------------------------------------------------------------------------- 1 | """ 2 | highlight blocks with pygments 3 | """ 4 | import re 5 | import os 6 | import html 7 | from pygments import highlight 8 | from pygments.formatters import HtmlFormatter 9 | from pygments.lexers import guess_lexer, get_lexer_by_name 10 | from barely.plugins import PluginBase 11 | from barely.core.ProcessingPipeline import write_file 12 | 13 | 14 | class Highlight(PluginBase): 15 | # make js-highlighting unnecessary by providing hightlighted pure html/css code blocks 16 | 17 | def __init__(self): 18 | super().__init__() 19 | standard_config = { 20 | "PRIORITY": 20, 21 | "CLASS_PREFIX": "hl", 22 | "LINE_NOS": "table", 23 | "TABSIZE": 4, 24 | "ENCODING": "utf-8", 25 | "THEME": "default", 26 | "LEXER": "", 27 | "ASSETS_DIR": "assets" 28 | } 29 | try: 30 | self.plugin_config = standard_config | self.config["HIGHLIGHT"] 31 | except KeyError: 32 | self.plugin_config = standard_config 33 | 34 | def register(self): 35 | return "Highlight", self.plugin_config["PRIORITY"], [self.config["PAGE_EXT"]] 36 | 37 | def action(self, *args, **kwargs): 38 | if "item" in kwargs: 39 | item = kwargs["item"] 40 | self.additional_styles = set() 41 | 42 | # accept page-level config for this plugin 43 | try: 44 | self.page_config = self.plugin_config | item["meta"]["highlight"] 45 | except KeyError: 46 | self.page_config = self.plugin_config.copy() 47 | 48 | item["content"] = re.sub(r"
.*?)\")?>(?P.*?)
", self._handle_code, item["content"], flags=re.S) 49 | item["action"] = "rendered, highlighted" 50 | item["additional_styles"] = list(self.additional_styles) 51 | yield item 52 | 53 | def _handle_code(self, match): 54 | code = html.unescape(match.group("code")) 55 | 56 | # if no lexer is set anywhere: guess it 57 | lexer_args = { 58 | "tabsize": self.page_config["TABSIZE"], 59 | "encoding": self.page_config["ENCODING"] 60 | } 61 | lexer = guess_lexer(code, **lexer_args) 62 | 63 | # use global (or page-level!) config lexer, if set 64 | try: 65 | lexer = get_lexer_by_name(self.page_config["LEXER"], **lexer_args) 66 | except Exception: 67 | pass 68 | 69 | # "best case": lexer is set right in the code snippet; obviously use it 70 | try: 71 | lexer_name = match.group("lang") 72 | lexer = get_lexer_by_name(lexer_name, **lexer_args) 73 | except Exception: 74 | pass 75 | 76 | formatter_args = { 77 | "classprefix": self.page_config["CLASS_PREFIX"], 78 | "linenos": self.page_config["LINE_NOS"], 79 | "style": self.page_config["THEME"] 80 | } 81 | formatter = HtmlFormatter(**formatter_args) 82 | 83 | css_path = os.path.join(self.config["ROOT"]["DEV"], self.page_config["ASSETS_DIR"], "highlight", self.page_config["THEME"]) + ".css" 84 | if not os.path.exists(css_path): 85 | css = formatter.get_style_defs(self.page_config["CLASS_PREFIX"]) 86 | self.additional_styles.add(f"/{self.page_config['ASSETS_DIR']}/highlight/{self.page_config['THEME']}.css") 87 | write_file([ 88 | { 89 | "destination": css_path, 90 | "origin": f"Code Hightlight Style: {self.page_config['THEME']}", 91 | "action": "generated styles", 92 | "output": css 93 | } 94 | ]) 95 | return f"<{self.page_config['CLASS_PREFIX']}>{highlight(code, lexer, formatter)}" 96 | -------------------------------------------------------------------------------- /barely/plugins/content/Highlight/test_highlight.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | from mock import patch 4 | from barely.plugins.content.Highlight.highlight import Highlight 5 | 6 | 7 | class TestHighlight(unittest.TestCase): 8 | 9 | maxDiff = None 10 | 11 | def test___init__(self): 12 | golden = { 13 | "PRIORITY": 20, 14 | "CLASS_PREFIX": "hl", 15 | "LINE_NOS": "table", 16 | "TABSIZE": 4, 17 | "ENCODING": "utf-8", 18 | "THEME": "default", 19 | "LEXER": "", 20 | "ASSETS_DIR": "assets" 21 | } 22 | 23 | hl = Highlight() 24 | self.assertDictEqual(golden, hl.plugin_config) 25 | 26 | hl.config["HIGHLIGHT"] = {"PRIORITY": -1} 27 | hl.__init__() 28 | 29 | golden["PRIORITY"] = -1 30 | self.assertDictEqual(golden, hl.plugin_config) 31 | 32 | # reset 33 | del hl.config["HIGHLIGHT"] 34 | hl.__init__() 35 | 36 | def test_register(self): 37 | hl = Highlight() 38 | name, prio, ext = hl.register() 39 | 40 | self.assertEqual(name, "Highlight") 41 | self.assertEqual(prio, 20) 42 | self.assertEqual(ext, ["md"]) 43 | 44 | @patch("barely.plugins.content.Highlight.highlight.write_file") 45 | def test_action(self, write_file): 46 | item = { 47 | "action": "rendered", 48 | "content": """ 49 |

 50 |                 def test_register(self):
 51 |                 hl = Highlight()
 52 |                 name, prio, ext = hl.register()
 53 | 
 54 |                 self.assertEqual(name, "Highlight")
 55 |                 self.assertEqual(prio, 20)
 56 |                 self.assertEqual(ext, ["md"])
57 | """ 58 | } 59 | 60 | golden_hl = """ 61 |
1\n2\n3\n4\n5\n6\n7
 63 |         
def test_register(self):\n        hl =
 65 |         Highlight()\n        name, prio
 66 |         , ext = hl.
 67 |         register()\n\n        self.
 68 |         assertEqual(name, "Highlight")\n        self.assertEqual(
 70 |         prio, 20)\n        self
 71 |         .assertEqual(ext,
 72 |         ["md"])\n
\n
73 | """ 74 | 75 | hl = Highlight() 76 | hl.plugin_config["LEXER"] = "python" 77 | 78 | result = list(hl.action(item=item.copy()))[0] 79 | 80 | self.assertEqual(re.sub(r'[\s+]', '', golden_hl), re.sub(r'[\s+]', '', result["content"])) 81 | self.assertEqual("rendered, highlighted", result["action"]) 82 | self.assertListEqual(["/assets/highlight/default.css"], result["additional_styles"]) 83 | self.assertTrue(write_file.called) 84 | 85 | write_file.reset_mock() 86 | 87 | item = { 88 | "action": "rendered", 89 | "content": """ 90 |

 91 |             fn main() {
 92 |                 println!("Hello World!");
 93 |             }
94 | """, 95 | "meta": { 96 | "highlight": { 97 | "THEME": "pastie" 98 | } 99 | } 100 | } 101 | 102 | golden_hl = """ 103 |
1\n2\n
104 |         3
fn 
105 |         main() {\n
106 |         println!("Hello World!");
107 |         \n            }\n
\n
108 | """ 109 | 110 | result = list(hl.action(item=item.copy()))[0] 111 | 112 | self.assertEqual(re.sub(r'[\s+]', '', golden_hl), re.sub(r'[\s+]', '', result["content"])) 113 | self.assertEqual("rendered, highlighted", result["action"]) 114 | self.assertListEqual(["/assets/highlight/pastie.css"], result["additional_styles"]) 115 | self.assertTrue(write_file.called) 116 | -------------------------------------------------------------------------------- /barely/plugins/content/Minify/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/Minify/__init__.py -------------------------------------------------------------------------------- /barely/plugins/content/Minify/minify.py: -------------------------------------------------------------------------------- 1 | """ 2 | Minify provides functons to minimize 3 | javascript. It also functions 4 | as a sass/scss parser. 5 | """ 6 | import os 7 | import sass 8 | from calmjs.parse import es5, exceptions as js_exceptions 9 | from calmjs.parse.unparsers.es5 import minify_print 10 | from barely.plugins import PluginBase 11 | 12 | 13 | class Minify(PluginBase): 14 | # Minify provides functions to compile and reduce scss and js in size 15 | 16 | def __init__(self): 17 | super().__init__() 18 | 19 | standard_config = { 20 | "PRIORITY": 3, 21 | "JS_OBFUSCATE": True, 22 | "JS_OBFUSCATE_GLOBALS": True, 23 | "CSS_INCLUDE_COMMENTS": False, 24 | "CSS_OUTPUT_STYLE": "compressed" 25 | } 26 | try: 27 | self.plugin_config = standard_config | self.config["MINIFY"] 28 | except KeyError: 29 | self.plugin_config = standard_config 30 | self.func_map = { 31 | "js": self.minimize_js, 32 | "sass,scss": self.minimize_css 33 | } 34 | self.register_for = sum([group.split(",") for group in self.func_map.keys()], []) 35 | 36 | def register(self): 37 | return "Minify", self.plugin_config["PRIORITY"], self.register_for 38 | 39 | def action(self, *args, **kwargs): 40 | if "item" in kwargs: 41 | item = kwargs["item"] 42 | for key, func in self.func_map.items(): 43 | if item["extension"] in key: 44 | yield func(item) 45 | 46 | def minimize_css(self, item): 47 | try: 48 | indented = True if item["extension"] == "sass" else False 49 | compiled = sass.compile(string=item["content_raw"], output_style=self.plugin_config["CSS_OUTPUT_STYLE"], 50 | indented=indented, source_comments=self.plugin_config["CSS_INCLUDE_COMMENTS"]) 51 | item["destination"] = os.path.splitext(item["destination"])[0] + ".css" 52 | item["action"] = "compiled" 53 | item["output"] = compiled 54 | except sass.CompileError as e: 55 | self.logger.error(f"SASS Compile {e}") 56 | return item 57 | 58 | def minimize_js(self, item): 59 | try: 60 | minified = minify_print(es5(item["output"]), obfuscate=self.plugin_config["JS_OBFUSCATE"], 61 | obfuscate_globals=self.plugin_config["JS_OBFUSCATE_GLOBALS"]) 62 | item["output"] = minified 63 | item["action"] = "compiled" 64 | except js_exceptions.ECMASyntaxError as e: 65 | self.logger.error(f"JS Syntax Error: {e}") 66 | return item 67 | -------------------------------------------------------------------------------- /barely/plugins/content/Minify/test_minify.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from mock import patch 3 | from unittest.mock import MagicMock 4 | from barely.plugins.content.Minify.minify import Minify 5 | 6 | 7 | class TestMinify(unittest.TestCase): 8 | 9 | def test___init__(self): 10 | mini = Minify() 11 | 12 | standard_config = { 13 | "PRIORITY": 3, 14 | "JS_OBFUSCATE": True, 15 | "JS_OBFUSCATE_GLOBALS": True, 16 | "CSS_INCLUDE_COMMENTS": False, 17 | "CSS_OUTPUT_STYLE": "compressed" 18 | } 19 | 20 | self.assertDictEqual(standard_config, mini.plugin_config) 21 | self.assertListEqual(["js", "sass", "scss"], mini.register_for) 22 | 23 | def test_register(self): 24 | mini = Minify() 25 | name, prio, ext = mini.register() 26 | 27 | self.assertEqual(name, "Minify") 28 | self.assertEqual(prio, 3) 29 | self.assertEqual(ext, ["js", "sass", "scss"]) 30 | 31 | @patch("barely.plugins.content.Minify.minify.minify_print") 32 | @patch("barely.plugins.content.Minify.minify.es5") 33 | @patch("barely.plugins.content.Minify.minify.sass") 34 | def test_action(self, sass, es5, minifyjs): 35 | sass.compile = MagicMock(return_value="compiled") 36 | minifyjs.return_value = "smallerjs" 37 | 38 | item = { 39 | "destination": "", 40 | "output": "", 41 | "extension": "", 42 | "content_raw": "" 43 | } 44 | 45 | mini = Minify() 46 | 47 | # SASS 48 | item["destination"] = "style.sass" 49 | item["extension"] = "sass" 50 | result = list(mini.action(item=item.copy()))[0] 51 | self.assertEqual(result["destination"], "style.css") 52 | self.assertEqual(result["output"], "compiled") 53 | self.assertEqual(result["action"], "compiled") 54 | 55 | # JS 56 | item["extension"] = "js" 57 | result = list(mini.action(item=item.copy()))[0] 58 | self.assertEqual(result["output"], "smallerjs") 59 | self.assertEqual(result["action"], "compiled") 60 | -------------------------------------------------------------------------------- /barely/plugins/content/Pixelizer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/Pixelizer/__init__.py -------------------------------------------------------------------------------- /barely/plugins/content/Pixelizer/pixelizer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Pixelizer resizes, compresses, and 3 | converts images into web-friendly 4 | formats. It also transforms 5 | tags in tags with fallback. 6 | """ 7 | import re 8 | import os 9 | from PIL import Image 10 | from barely.plugins import PluginBase 11 | 12 | 13 | class Pixelizer(PluginBase): 14 | # Minify provides functions to compile and reduce scss and js in size 15 | 16 | def __init__(self): 17 | super().__init__() 18 | try: 19 | standard_config = { 20 | "PRIORITY": 3, 21 | "TARGETS": [ 22 | "lg 1000 70", 23 | "md 650 70", 24 | "sm 300 70" 25 | ], 26 | "LAYOUTS": [ 27 | "(max-width: 1000px) 100vw", 28 | "1000px" 29 | ] 30 | } 31 | self.plugin_config = standard_config | self.config["PIXELIZER"] 32 | 33 | for i, t in enumerate(self.plugin_config["TARGETS"]): 34 | slug, width, quality = t.split() 35 | self.plugin_config["TARGETS"][i] = { 36 | "slug": slug, 37 | "width": int(width), 38 | "quality": int(quality) 39 | } 40 | 41 | self.func_map = { 42 | "png,jpg,jpeg,tif,tiff,bmp": self.process_image, 43 | self.config["PAGE_EXT"]: self.process_page 44 | } 45 | self.register_for = sum([group.split(",") for group in self.func_map.keys()], []) 46 | except KeyError: 47 | self.plugin_config = {"PRIORITY": -1} 48 | self.register_for = [] 49 | 50 | def register(self): 51 | return "Pixelizer", self.plugin_config["PRIORITY"], self.register_for 52 | 53 | def action(self, *args, **kwargs): 54 | if "item" in kwargs: 55 | item = kwargs["item"] 56 | for key, func in self.func_map.items(): 57 | if item["extension"] in key: 58 | yield from func(item) 59 | 60 | def process_image(self, item): 61 | item["action"] = "processed" 62 | filename = os.path.splitext(item["destination"])[0] 63 | 64 | for type in ["webp", item["extension"]]: 65 | for target in self.plugin_config["TARGETS"]: 66 | self.logger.debug(f"Started processing for type: {type}, target: {target['slug']}") 67 | variant = item.copy() 68 | variant["image"] = item["image"].copy() 69 | try: 70 | _, original_y = item["image"].size 71 | target_x = target["width"] 72 | size = target_x, original_y 73 | self.logger.debug(f"Original size: {item['image'].size}, New size: {size}") 74 | variant["image"].thumbnail(size, Image.LANCZOS) 75 | variant["quality"] = target["quality"] 76 | variant["destination"] = f"{filename}-{target['slug']}.{type}" 77 | variant["extension"] = type 78 | except Exception as e: 79 | self.logger.error(f"An Error occured while handling the image: {e}") 80 | self.logger.debug(f"Finished processing for type: {type}, target: {target['slug']}") 81 | yield variant 82 | 83 | # copy the original as fallback 84 | item["copymode"] = True 85 | yield item 86 | 87 | def process_page(self, item): 88 | try: 89 | if "none" in item["meta"]["PIXELIZER"]: 90 | yield item 91 | return 92 | except KeyError: 93 | pass 94 | 95 | try: 96 | self.item_config = self.plugin_config.copy() | { 97 | "LAYOUTS": item["meta"]["PIXELIZER"] 98 | } 99 | except KeyError: 100 | self.item_config = self.plugin_config.copy() 101 | 102 | item["content"] = re.sub(r".*)[\"'])?\s+src=[\"'](?P\S+)\.(?P[a-zA-Z]+)[\"'](?:\s+alt=[\"'](?P.*)[\"'])?\s*[/]?>", self._generate_tag, item["content"]) 103 | 104 | yield item 105 | 106 | def _generate_tag(self, match): 107 | file = match.group("file") 108 | ext = match.group("ext") 109 | alt = "" 110 | 111 | if match.group("alt2") is not None: 112 | alt = match.group("alt2") 113 | elif match.group("alt1") is not None: 114 | alt = match.group("alt1") 115 | 116 | if ext not in self.register_for: 117 | return f"\"{alt}\"" 118 | 119 | sizes = ", ".join(self.item_config["LAYOUTS"]) 120 | 121 | components = [""] 122 | 123 | for type in ["webp", ext]: 124 | c = f"" 132 | 133 | components.append(c) 134 | 135 | components.append(f"\"{alt}\"") 136 | components.append("") 137 | 138 | return "\n".join(components) 139 | -------------------------------------------------------------------------------- /barely/plugins/content/Pixelizer/test_pixelizer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | from mock import patch 4 | from unittest.mock import MagicMock 5 | from barely.plugins.content.Pixelizer.pixelizer import Pixelizer 6 | 7 | 8 | class TestPixelizer(unittest.TestCase): 9 | 10 | def test___init__(self): 11 | pix = Pixelizer() 12 | self.assertDictEqual({"PRIORITY": -1}, pix.plugin_config) 13 | self.assertListEqual([], pix.register_for) 14 | 15 | golden = { 16 | "PRIORITY": 2, 17 | "TARGETS": [ 18 | { 19 | "slug": "lg", 20 | "width": 1000, 21 | "quality": 70 22 | }, 23 | { 24 | "slug": "md", 25 | "width": 650, 26 | "quality": 70 27 | }, 28 | { 29 | "slug": "sm", 30 | "width": 300, 31 | "quality": 70 32 | } 33 | ], 34 | "LAYOUTS": [ 35 | "(max-width: 1000px) 100vw", 36 | "1000px" 37 | ] 38 | } 39 | 40 | golden_register_for = ["png", "jpg", "jpeg", "tif", "tiff", "bmp", "md"] 41 | 42 | pix.config["PIXELIZER"] = {"PRIORITY": 2} 43 | pix.__init__() 44 | 45 | self.assertDictEqual(golden, pix.plugin_config) 46 | self.assertListEqual(golden_register_for, pix.register_for) 47 | 48 | # reset 49 | del pix.config["PIXELIZER"] 50 | pix.__init__() 51 | 52 | def test_register(self): 53 | pix = Pixelizer() 54 | name, prio, ext = pix.register() 55 | 56 | self.assertEqual(name, "Pixelizer") 57 | self.assertEqual(prio, -1) 58 | self.assertEqual(ext, []) 59 | 60 | @patch("barely.plugins.content.Pixelizer.pixelizer.Image") 61 | def test_action(self, image): 62 | # Testing the image handling 63 | image.thumbnail = MagicMock() 64 | image.size = 1920, 720 65 | 66 | item = { 67 | "destination": "/path/to/somewhere.png", 68 | "output": "", 69 | "extension": "png", 70 | "content_raw": "", 71 | "image": image 72 | } 73 | 74 | pix = Pixelizer() 75 | pix.config["PIXELIZER"] = {"PRIORITY": 1} 76 | pix.__init__() 77 | 78 | # Image 79 | result = list(pix.action(item=item.copy())) 80 | 81 | self.assertEqual(len(result), 7) 82 | 83 | self.assertEqual(result[0]["quality"], 70) 84 | self.assertEqual(result[0]["destination"], "/path/to/somewhere-lg.webp") 85 | self.assertEqual(result[0]["extension"], "webp") 86 | 87 | self.assertEqual(result[5]["quality"], 70) 88 | self.assertEqual(result[5]["destination"], "/path/to/somewhere-sm.png") 89 | self.assertEqual(result[5]["extension"], "png") 90 | 91 | self.assertEqual(result[6]["destination"], "/path/to/somewhere.png") 92 | self.assertEqual(result[6]["action"], "processed") 93 | self.assertTrue(result[6]["copymode"]) 94 | 95 | # Testing the page handling 96 | 97 | item = { 98 | "extension": "md", 99 | "content": """ 100 |
101 | Test Image 102 |
103 | """ 104 | } 105 | 106 | golden = """ 107 |
108 | 109 | 110 | 111 | Test Image 112 | 113 |
114 | """ 115 | result = list(pix.action(item=item.copy()))[0] 116 | 117 | self.assertEqual(re.sub(r'[\s+]', '', result["content"]), re.sub(r'[\s+]', '', golden)) 118 | 119 | # "none" 120 | item = { 121 | "extension": "md", 122 | "content": """ 123 |
124 | Test Image 125 |
126 | """, 127 | "meta": { 128 | "PIXELIZER": "none" 129 | } 130 | } 131 | result = list(pix.action(item=item.copy()))[0] 132 | self.assertDictEqual(result, item) 133 | 134 | # unsupported type 135 | item = { 136 | "extension": "md", 137 | "content": """ 138 |
139 | Test Image 140 |
141 | """ 142 | } 143 | 144 | golden = { 145 | "extension": "md", 146 | "content": """ 147 |
148 | Test Image 149 |
150 | """ 151 | } 152 | result = list(pix.action(item=item.copy()))[0] 153 | self.assertDictEqual(result, golden) 154 | 155 | del pix.config["PIXELIZER"] 156 | pix.__init__() 157 | -------------------------------------------------------------------------------- /barely/plugins/content/ReadingTime/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/ReadingTime/__init__.py -------------------------------------------------------------------------------- /barely/plugins/content/ReadingTime/readingtime.py: -------------------------------------------------------------------------------- 1 | """ 2 | estimate how long it will take to read a page 3 | """ 4 | from barely.plugins import PluginBase 5 | 6 | 7 | class ReadingTime(PluginBase): 8 | # very rough estimate of the reading time in minutes 9 | 10 | def __init__(self): 11 | super().__init__() 12 | standard_config = { 13 | "PRIORITY": 850, 14 | "WPM_FAST": 265, 15 | "WPM_SLOW": 90, 16 | "SEPARATOR": " - " 17 | } 18 | try: 19 | self.plugin_config = standard_config | self.config["READING_TIME"] 20 | except KeyError: 21 | self.plugin_config = standard_config 22 | 23 | def register(self): 24 | return "ReadingTime", self.plugin_config["PRIORITY"], [self.config["PAGE_EXT"]] 25 | 26 | def action(self, *args, **kwargs): 27 | if "item" in kwargs: 28 | item = kwargs["item"] 29 | word_count = len(item["content_raw"].split()) 30 | slow = word_count // int(self.plugin_config["WPM_SLOW"]) 31 | fast = word_count // int(self.plugin_config["WPM_FAST"]) 32 | if slow != fast: 33 | item["meta"]["reading_time"] = f"{fast}{self.plugin_config['SEPARATOR']}{slow}" 34 | else: 35 | item["meta"]["reading_time"] = str(slow) 36 | yield item 37 | -------------------------------------------------------------------------------- /barely/plugins/content/ReadingTime/test_readingtime.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from barely.plugins.content.ReadingTime.readingtime import ReadingTime 3 | 4 | 5 | class TestReadingTime(unittest.TestCase): 6 | 7 | def test___init__(self): 8 | golden = { 9 | "PRIORITY": 850, 10 | "WPM_FAST": 265, 11 | "WPM_SLOW": 90, 12 | "SEPARATOR": " - " 13 | } 14 | rt = ReadingTime() 15 | self.assertDictEqual(golden, rt.plugin_config) 16 | 17 | rt.config["READING_TIME"] = {"PRIORITY": -1} 18 | rt.__init__() 19 | 20 | golden["PRIORITY"] = -1 21 | self.assertDictEqual(golden, rt.plugin_config) 22 | 23 | # reset 24 | del rt.config["READING_TIME"] 25 | rt.__init__() 26 | 27 | def test_register(self): 28 | rt = ReadingTime() 29 | name, prio, ext = rt.register() 30 | 31 | self.assertEqual(name, "ReadingTime") 32 | self.assertEqual(prio, 850) 33 | self.assertEqual(ext, ["md"]) 34 | 35 | def test_action(self): 36 | item = { 37 | "content_raw": """ 38 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 39 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit 40 | amet. 41 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 42 | voluptua. 43 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet 44 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam 45 | voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet 46 | 47 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et 48 | accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit ame, 49 | consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 50 | 51 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum 52 | iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto 53 | odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 54 | 55 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer 56 | """, 57 | "meta": {} 58 | } 59 | 60 | rt = ReadingTime() 61 | result = list(rt.action(item=item))[0] 62 | 63 | self.assertEqual(result["meta"]["reading_time"], "1 - 3") 64 | -------------------------------------------------------------------------------- /barely/plugins/content/Timestamps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/Timestamps/__init__.py -------------------------------------------------------------------------------- /barely/plugins/content/Timestamps/test_timestamps.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from mock import patch 3 | from barely.plugins.content.Timestamps.timestamps import Timestamps 4 | 5 | 6 | class TestTimestamps(unittest.TestCase): 7 | 8 | def test___init__(self): 9 | ts = Timestamps() 10 | self.assertDictEqual({"PRIORITY": -1}, ts.plugin_config) 11 | 12 | ts.config["TIMESTAMPS"] = {"PRIORITY": 2} 13 | ts.__init__() 14 | 15 | golden = { 16 | "PRIORITY": 2, 17 | "FORMAT": "%d.%m.%Y" 18 | } 19 | 20 | self.assertDictEqual(golden, ts.plugin_config) 21 | 22 | # reset 23 | del ts.config["TIMESTAMPS"] 24 | ts.__init__() 25 | 26 | def test_register(self): 27 | ts = Timestamps() 28 | name, prio, ext = ts.register() 29 | 30 | self.assertEqual(name, "Timestamps") 31 | self.assertEqual(prio, -1) 32 | self.assertEqual(ext, ["md"]) 33 | 34 | @patch("barely.plugins.content.Timestamps.timestamps.getctime") 35 | @patch("barely.plugins.content.Timestamps.timestamps.getmtime") 36 | def test_action(self, getmtime, getctime): 37 | getctime.return_value = 1622928404 38 | getmtime.return_value = 1622928404 39 | 40 | ts = Timestamps() 41 | ts.plugin_config = { 42 | "PRIORITY": 3, 43 | "FORMAT": "%d.%m.%Y" 44 | } 45 | 46 | item = { 47 | "origin": "", 48 | "meta": {} 49 | } 50 | 51 | result = list(ts.action(item=item))[0] 52 | 53 | self.assertEqual(result["meta"]["created"], "05.06.2021") 54 | self.assertEqual(result["meta"]["edited"], "05.06.2021") 55 | 56 | item = { 57 | "origin": "", 58 | "meta": { 59 | "created": "custom", 60 | "edited": "custom" 61 | } 62 | } 63 | 64 | result = list(ts.action(item=item))[0] 65 | 66 | self.assertEqual(result["meta"]["created"], "custom") 67 | self.assertEqual(result["meta"]["edited"], "custom") 68 | -------------------------------------------------------------------------------- /barely/plugins/content/Timestamps/timestamps.py: -------------------------------------------------------------------------------- 1 | """ 2 | timestamp pages with their creation date 3 | and latest edit date, however do 4 | respect user overrides 5 | """ 6 | from datetime import datetime 7 | from os.path import getctime, getmtime 8 | from barely.plugins import PluginBase 9 | 10 | 11 | class Timestamps(PluginBase): 12 | # Timestamps guesses what the user is interested in and sets timestamps accordingly 13 | 14 | def __init__(self): 15 | super().__init__() 16 | try: 17 | standard_config = { 18 | "PRIORITY": 3, 19 | "FORMAT": "%d.%m.%Y" 20 | } 21 | self.plugin_config = standard_config | self.config["TIMESTAMPS"] 22 | except KeyError: 23 | self.plugin_config = {"PRIORITY": -1} 24 | 25 | def register(self): 26 | return "Timestamps", self.plugin_config["PRIORITY"], [self.config["PAGE_EXT"]] 27 | 28 | def action(self, *args, **kwargs): 29 | if "item" in kwargs: 30 | item = kwargs["item"] 31 | 32 | try: 33 | ctime = datetime.fromtimestamp(getctime(item["origin"])).strftime(self.plugin_config["FORMAT"]) 34 | mtime = datetime.fromtimestamp(getmtime(item["origin"])).strftime(self.plugin_config["FORMAT"]) 35 | 36 | if "created" not in item["meta"]: # if not set by user, set Created Time 37 | item["meta"]["created"] = ctime 38 | if "edited" not in item["meta"]: # if not set by user, set Modified Time 39 | item["meta"]["edited"] = mtime 40 | except FileNotFoundError: 41 | self.logger.debug(f"{item['origin']} not found.") 42 | 43 | yield item 44 | -------------------------------------------------------------------------------- /barely/plugins/content/ToC/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/ToC/__init__.py -------------------------------------------------------------------------------- /barely/plugins/content/ToC/test_toc.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unittest 3 | from barely.plugins.content.ToC.toc import ToC 4 | 5 | 6 | class TestToC(unittest.TestCase): 7 | 8 | maxDiff = None 9 | 10 | def test___init__(self): 11 | golden = { 12 | "PRIORITY": 2, 13 | "MIN_DEPTH": 1, 14 | "MAX_DEPTH": 4, 15 | "LIST_ELEMENT": "ul" 16 | } 17 | 18 | toc = ToC() 19 | self.assertDictEqual(golden, toc.plugin_config) 20 | 21 | toc.config["TOC"] = {"PRIORITY": 12} 22 | toc.__init__() 23 | golden["PRIORITY"] = 12 24 | 25 | self.assertDictEqual(golden, toc.plugin_config) 26 | 27 | # reset 28 | del toc.config["TOC"] 29 | toc.__init__() 30 | 31 | def test_register(self): 32 | toc = ToC() 33 | name, prio, ext = toc.register() 34 | 35 | self.assertEqual(name, "ToC") 36 | self.assertEqual(prio, 2) 37 | self.assertEqual(ext, ["md"]) 38 | 39 | def test_action(self): 40 | item = { 41 | "content": """ 42 | 43 | 44 | 45 | 46 |

A 2

47 |

48 |

B 2

49 |

50 |

C 3

51 |

52 |

D 2

53 |

54 |

E 3

55 |

56 |

F 4

57 |

58 |

G 4

59 |

60 |

H 3

61 |

62 | 63 | 64 | """, 65 | "meta": {} 66 | } 67 | 68 | golden = { 69 | "content": """ 70 | 71 | 72 | 73 | 74 |

A 2

75 |

76 |

B 2

77 |

78 |

C 3

79 |

80 |

D 2

81 |

82 |

E 3

83 |

84 |

F 4

85 |

86 |

G 4

87 |

88 |

H 3

89 |

90 | 91 | 92 | """, 93 | "meta": { 94 | "toc": """ 95 |
96 |
    97 |
  • 98 |
      99 |
    • A 2
    • 100 |
    • B 2 101 |
        102 |
      • C 3
      • 103 |
      104 |
    • 105 |
    • D 2 106 |
        107 |
      • E 3 108 | 112 |
      • 113 |
      • H 3
      • 114 |
      115 |
    • 116 |
    117 |
  • 118 |
119 |
120 | """ 121 | } 122 | } 123 | 124 | toc = ToC() 125 | result = list(toc.action(item=item)) 126 | self.assertEqual(1, len(result)) 127 | 128 | self.assertEqual(item["content"], golden["content"]) 129 | self.assertEqual(re.sub(r'[\s+]', '', item["meta"]["toc"]), re.sub(r'[\s+]', '', golden["meta"]["toc"])) 130 | -------------------------------------------------------------------------------- /barely/plugins/content/ToC/toc.py: -------------------------------------------------------------------------------- 1 | """ 2 | build a Table of Contents 3 | from the heading, up to a 4 | specified depth. insert IDs 5 | into the tags to be able 6 | to link to them 7 | """ 8 | import re 9 | from barely.plugins import PluginBase 10 | 11 | 12 | class ToC(PluginBase): 13 | # build a linked table of contents out of the html headings 14 | 15 | TOC = [] 16 | 17 | def __init__(self): 18 | super().__init__() 19 | standard_config = { 20 | "PRIORITY": 2, 21 | "MIN_DEPTH": 1, 22 | "MAX_DEPTH": 4, 23 | "LIST_ELEMENT": "ul" 24 | } 25 | try: 26 | self.plugin_config = standard_config | self.config["TOC"] 27 | except KeyError: 28 | self.plugin_config = standard_config 29 | 30 | def register(self): 31 | return "ToC", self.plugin_config["PRIORITY"], [self.config["PAGE_EXT"]] 32 | 33 | def action(self, *args, **kwargs): 34 | if "item" in kwargs: 35 | self.TOC = [] 36 | item = kwargs["item"] 37 | # should the generation be attempted a second time, the result will be an empty ToC! 38 | if "toc" not in item["meta"] and "parent_meta" not in item["meta"]: 39 | item["content"] = re.sub(r"(.+)<\/h\d{1}>", self._handle_matches, item["content"]) 40 | item["meta"]["toc"] = "\n".join(self._generate_toc()) 41 | yield item 42 | 43 | def _handle_matches(self, match): 44 | level = match.group(1) 45 | heading = match.group(2) 46 | slug = re.sub(r"[^0-9a-zA-Z]+", "-", heading).lower() 47 | if int(level) >= self.plugin_config["MIN_DEPTH"] and int(level) <= self.plugin_config["MAX_DEPTH"]: 48 | self.TOC.append((int(level), heading, slug)) 49 | return f'{heading}' 50 | else: 51 | return match.group(0) 52 | 53 | def _generate_toc(self): 54 | cur_lvl = self.plugin_config["MIN_DEPTH"] - 1 55 | 56 | yield '
' 57 | for index, (level, heading, slug) in enumerate(self.TOC): 58 | 59 | opened_layer = False 60 | while level > cur_lvl: 61 | if opened_layer: 62 | yield "
  • " 63 | yield f"<{self.plugin_config['LIST_ELEMENT']}>" 64 | opened_layer = True 65 | cur_lvl += 1 66 | 67 | while level < cur_lvl: 68 | yield f"
  • " 69 | cur_lvl -= 1 70 | 71 | yield "
  • " 72 | yield f'{heading}' 73 | if len(self.TOC) - 1 == index or self.TOC[index + 1][0] <= cur_lvl: 74 | yield "
  • " 75 | 76 | while self.plugin_config["MIN_DEPTH"] <= cur_lvl: 77 | yield f"" 78 | if cur_lvl != self.plugin_config["MIN_DEPTH"]: 79 | yield "" 80 | cur_lvl -= 1 81 | yield "
    " 82 | -------------------------------------------------------------------------------- /barely/plugins/content/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/content/__init__.py -------------------------------------------------------------------------------- /barely/plugins/publication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/publication/__init__.py -------------------------------------------------------------------------------- /barely/plugins/publication/sftp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/plugins/publication/sftp/__init__.py -------------------------------------------------------------------------------- /barely/plugins/publication/sftp/sftp.py: -------------------------------------------------------------------------------- 1 | """ 2 | transfer only the changed files 3 | (save bandwidth) to an sftp server 4 | """ 5 | import os 6 | import pysftp 7 | from barely.plugins import PluginBase 8 | 9 | 10 | class SFTP(PluginBase): 11 | """ copy to remote server; only new/changed though """ 12 | 13 | def __init__(self): 14 | super().__init__() 15 | try: 16 | standard_config = { 17 | "PRIORITY": 90, 18 | "HOSTNAME": "", 19 | "USER": "", 20 | "PASSWORD": "", 21 | "KEY": "", 22 | "ROOT": "" 23 | } 24 | self.plugin_config = standard_config | self.config["SFTP"] 25 | except KeyError: 26 | self.plugin_config = {"PRIORITY": -1} 27 | 28 | def register(self): 29 | return "sftp", self.plugin_config["PRIORITY"] 30 | 31 | def action(self, *args, **kwargs): 32 | try: 33 | if self.plugin_config["KEY"] != "": 34 | conn = pysftp.Connection(self.plugin_config["HOSTNAME"], 35 | username=self.plugin_config["USER"], 36 | private_key=self.plugin_config["KEY"], 37 | private_key_pass=True) 38 | else: 39 | conn = pysftp.Connection(self.plugin_config["HOSTNAME"], 40 | username=self.plugin_config["USER"], 41 | password=self.plugin_config["PASSWORD"]) 42 | 43 | conn.put_r(os.path.join(self.config["ROOT"]["WEB"], ""), self.plugin_config["ROOT"], preserve_mtime=True) 44 | conn.close() 45 | self.logger.info(f"published via SFTP to {self.plugin_config['HOSTNAME']}") 46 | except KeyError: 47 | self.logger.error("SFTP configuration is incomplete or invalid.") 48 | -------------------------------------------------------------------------------- /barely/plugins/publication/sftp/test_sftp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from mock import patch 3 | from barely.plugins.publication.sftp.sftp import SFTP 4 | 5 | 6 | class TestSFTP(unittest.TestCase): 7 | 8 | def test___init__(self): 9 | sftp = SFTP() 10 | self.assertDictEqual({"PRIORITY": -1}, sftp.plugin_config) 11 | 12 | sftp.config["SFTP"] = {"PRIORITY": 2} 13 | sftp.__init__() 14 | 15 | golden = { 16 | "PRIORITY": 2, 17 | "HOSTNAME": "", 18 | "USER": "", 19 | "PASSWORD": "", 20 | "KEY": "", 21 | "ROOT": "" 22 | } 23 | 24 | self.assertDictEqual(golden, sftp.plugin_config) 25 | 26 | # reset 27 | del sftp.config["SFTP"] 28 | sftp.__init__() 29 | 30 | def test_register(self): 31 | sftp = SFTP() 32 | name, prio = sftp.register() 33 | 34 | self.assertEqual(name, "sftp") 35 | self.assertEqual(prio, -1) 36 | 37 | @patch("barely.plugins.publication.sftp.sftp.pysftp.Connection") 38 | def test_action(self, Connection): 39 | sftp = SFTP() 40 | sftp.plugin_config = { 41 | "PRIORITY": 2, 42 | "HOSTNAME": "", 43 | "USER": "", 44 | "PASSWORD": "", 45 | "KEY": "", 46 | "ROOT": "" 47 | } 48 | sftp.action() 49 | Connection.assert_called_with("", username="", password="") 50 | 51 | Connection.reset_mock() 52 | 53 | sftp.plugin_config = { 54 | "PRIORITY": 2, 55 | "HOSTNAME": "", 56 | "USER": "", 57 | "PASSWORD": "", 58 | "KEY": "key", 59 | "ROOT": "" 60 | } 61 | sftp.action() 62 | Connection.assert_called_with("", username="", private_key="key", private_key_pass=True) 63 | -------------------------------------------------------------------------------- /barely/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/__init__.py -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/base.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/base.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/extendsdeeper.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/extendsdeeper.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/left.extendsbase.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/left.extendsbase.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/left.left.extendsbase.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/left.left.extendsbase.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/left.left.extendschild.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/left.left.extendschild.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/left.parentless.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/left.parentless.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/left.right.completelyalone.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/left.right.completelyalone.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/right.left.completelyalone.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/right.left.completelyalone.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/right.left.deeper.md: -------------------------------------------------------------------------------- 1 | include: "test.jpg" 2 | -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/right.right.extendsparentless.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/right.right.extendsparentless.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/templates/base.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/templates/base.html -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/templates/extendsdeeper.html: -------------------------------------------------------------------------------- 1 | {%extends 'right/left/deeper.html'%} 2 | 3 | -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/templates/left/extendsbase.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% include "left/parentless.html" %} 3 | -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/templates/left/left/extendsbase.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/templates/left/left/extendschild.html: -------------------------------------------------------------------------------- 1 | {% extends "left/extendsbase.html" %} 2 | -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/templates/left/parentless.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/templates/left/parentless.html -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/templates/left/right/completelyalone.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/templates/left/right/completelyalone.html -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/templates/right/left/completelyalone.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/templates/right/left/completelyalone.html -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/templates/right/left/deeper.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/affected/templates/right/left/deeper.html -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/affected/templates/right/right/extendsparentless.html: -------------------------------------------------------------------------------- 1 | {% include "left/parentless.html" %} 2 | -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/force/config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/force/config.yaml -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/force/dir/_subpage/subimage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/force/dir/_subpage/subimage.jpg -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/force/dir/_subpage/subpage.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/force/dir/_subpage/subpage.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/force/dir/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/force/dir/image.png -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/force/dir/page.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/force/dir/page.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/force/file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/force/file.txt -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/force/metadata.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/force/metadata.yaml -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/force/templates/template.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/force/templates/template.html -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/getparent/_sub/child.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/getparent/_sub/child.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/getparent/none/none/parentless.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/getparent/none/none/parentless.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/getparent/parent.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/getparent/parent.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/getparent/x_parent.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/getparent/x_parent.md -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/type/binary.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/type/binary.mp4 -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/type/file.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/type/file.css -------------------------------------------------------------------------------- /barely/tests/ressources/EventHandler/type/notype: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/EventHandler/type/notype -------------------------------------------------------------------------------- /barely/tests/ressources/PluginManager/content/P1.py: -------------------------------------------------------------------------------- 1 | from barely.plugins import PluginBase 2 | 3 | 4 | class POne(PluginBase): 5 | def register(self): 6 | return "P1", 3, ["pdf", "png"] 7 | 8 | def action(self, item): 9 | yield item 10 | -------------------------------------------------------------------------------- /barely/tests/ressources/PluginManager/content/P2.py: -------------------------------------------------------------------------------- 1 | from barely.plugins import PluginBase 2 | 3 | 4 | class PTwo(PluginBase): 5 | def register(self): 6 | return "P2", 1, ["md", "png"] 7 | 8 | def action(self, item): 9 | yield item 10 | -------------------------------------------------------------------------------- /barely/tests/ressources/PluginManager/empty/collateral: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/PluginManager/empty/collateral -------------------------------------------------------------------------------- /barely/tests/ressources/PluginManager/other/P3Dir/P3.py: -------------------------------------------------------------------------------- 1 | from barely.plugins import PluginBase 2 | 3 | 4 | class PThree(PluginBase): 5 | def register(self): 6 | return "P3", 3 7 | 8 | def action(self, item): 9 | yield item 10 | -------------------------------------------------------------------------------- /barely/tests/ressources/PluginManager/other/P4.py: -------------------------------------------------------------------------------- 1 | from barely.plugins import PluginBase 2 | 3 | 4 | class PFour(PluginBase): 5 | def register(self): 6 | return "P4", 1 7 | 8 | def action(self, item): 9 | yield item 10 | -------------------------------------------------------------------------------- /barely/tests/ressources/Plugins/AutoSEO/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/Plugins/AutoSEO/a.png -------------------------------------------------------------------------------- /barely/tests/ressources/Plugins/AutoSEO/b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/Plugins/AutoSEO/b.jpg -------------------------------------------------------------------------------- /barely/tests/ressources/Plugins/Gallery/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/Plugins/Gallery/a.png -------------------------------------------------------------------------------- /barely/tests/ressources/Plugins/Gallery/b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/Plugins/Gallery/b.jpg -------------------------------------------------------------------------------- /barely/tests/ressources/Plugins/Gallery/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/Plugins/Gallery/c.png -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/content_files/EMPTY.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/content_files/EMPTY.md -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/content_files/MULTI_YAML.md: -------------------------------------------------------------------------------- 1 | --- 2 | value: a 3 | --- 4 | value2: b 5 | --- 6 | # Title 7 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/content_files/ONE_YAML_ONE_MD.md: -------------------------------------------------------------------------------- 1 | --- 2 | value: a 3 | --- 4 | # Title 5 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/content_files/ONLY_MD.md: -------------------------------------------------------------------------------- 1 | # Title 2 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/content_files/ONLY_YAML.md: -------------------------------------------------------------------------------- 1 | --- 2 | value: a 3 | --- 4 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/delete/dir/collateral: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/delete/dir/collateral -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/delete/file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/delete/file.txt -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/move/fro.txt: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/move/fro/collateral: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/move/fro/collateral -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/move/fro2.txt: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/move/fro2/collateral: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/move/fro2/collateral -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/pipes/_subpage/subpage_dev.md: -------------------------------------------------------------------------------- 1 | Child 2 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/pipes/generic_dev.txt: -------------------------------------------------------------------------------- 1 | multi 2 | line 3 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/pipes/image_dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/pipes/image_dev.png -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/pipes/page_dev.md: -------------------------------------------------------------------------------- 1 | --- 2 | value: a 3 | --- 4 | # Title 5 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/pipes/page_with_child_dev.md: -------------------------------------------------------------------------------- 1 | --- 2 | modular: 3 | - subpage 4 | parent_meta: pm 5 | --- 6 | # Title Parent 7 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/pipes/subpage_dev.md: -------------------------------------------------------------------------------- 1 | --- 2 | value: a 3 | --- 4 | # Title 5 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/pipes/text_dev.txt: -------------------------------------------------------------------------------- 1 | multi 2 | line 3 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/subpages/_subpage/sub.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/subpages/_subpage/sub.md -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/template.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/template.md -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/template.subtemplate.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/template.subtemplate.md -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/templates/page_dev.html: -------------------------------------------------------------------------------- 1 | {{ value }} 2 | {{ content }} 3 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/templates/page_with_child_dev.html: -------------------------------------------------------------------------------- 1 | {{ content }} 2 | {{ sub_pages[0] }} 3 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/templates/subpage_dev.html: -------------------------------------------------------------------------------- 1 | {{ value }} 2 | {{ content }} 3 | {{ parent_meta }} 4 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/templates/template.html: -------------------------------------------------------------------------------- 1 | {{ content }} 2 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/test_load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/test_load.png -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/test_read.md: -------------------------------------------------------------------------------- 1 | multi 2 | line 3 | -------------------------------------------------------------------------------- /barely/tests/ressources/ProcessingPipeline/test_save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/ProcessingPipeline/test_save.png -------------------------------------------------------------------------------- /barely/tests/ressources/config.yaml: -------------------------------------------------------------------------------- 1 | ROOT: 2 | DEV: . 3 | WEB: web 4 | PAGE_EXT: md 5 | -------------------------------------------------------------------------------- /barely/tests/ressources/empty/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/barely/tests/ressources/empty/empty -------------------------------------------------------------------------------- /barely/tests/test_ChangeTracker.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from mock import patch 3 | from unittest.mock import MagicMock 4 | from watchdog.observers import Observer 5 | from watchdog.events import PatternMatchingEventHandler 6 | from barely.core.ChangeTracker import ChangeTracker 7 | 8 | 9 | class TestChangeTracker(unittest.TestCase): 10 | 11 | @classmethod 12 | def setUpClass(self): 13 | with patch.object(ChangeTracker, "__init__", lambda x: None): 14 | self.CT = ChangeTracker() 15 | 16 | def test___init__(self): 17 | EH_no = ChangeTracker() 18 | self.assertFalse(EH_no.handler_available) 19 | 20 | with patch("barely.core.ChangeTracker.ChangeTracker.register_handler") as reg: 21 | ChangeTracker(lambda x: x) 22 | self.assertTrue(reg.called) 23 | 24 | def test_register_handler(self): 25 | with patch.object(PatternMatchingEventHandler, "__init__", lambda v, w, x, y, z: None): 26 | with patch.object(Observer, "schedule") as obs: 27 | self.CT.register_handler(lambda x: None) 28 | self.assertTrue(obs.called) 29 | 30 | @patch("signal.getsignal") 31 | @patch("signal.signal") 32 | @patch("livereload.Server") 33 | @patch("multiprocessing.Process") 34 | @patch("watchdog.observers.Observer") 35 | def test_track(self, observer, server, serverprocess, signal, getsignal): 36 | self.CT.handler_available = False 37 | with self.assertRaises(Exception) as context: 38 | self.CT.track() 39 | self.assertTrue("No available handler. Not tracking." in str(context.exception)) 40 | 41 | self.CT.verbose = False 42 | self.CT.handler_available = True 43 | self.CT.observer = observer 44 | self.CT.observer.start = MagicMock() 45 | self.CT.liveserver.start = MagicMock() 46 | serverprocess.start = MagicMock() 47 | server.serve = MagicMock() 48 | server.watch = MagicMock() 49 | getsignal.return_value = None 50 | 51 | def loop_action(): 52 | self.CT.tracking = False 53 | with patch("barely.core.ChangeTracker.ChangeTracker.empty_buffer"): 54 | self.CT.track(loop_action) 55 | self.CT.liveserver.kill() 56 | self.assertTrue(self.CT.observer.start.called) 57 | self.assertTrue(signal.called) 58 | self.assertTrue(getsignal.called) 59 | self.assertTrue(server.watch) 60 | 61 | def test_buffer(self): 62 | self.CT.eventbuffer = [] 63 | 64 | event_1a = type('obj', (object,), {"src_path": 1}) 65 | event_1b = type('obj', (object,), {"dest_path": 1, "src_path": 123}) 66 | event_2 = type('obj', (object,), {"src_path": 2}) 67 | 68 | self.CT.buffer(event_1a) 69 | self.CT.buffer(event_2) 70 | self.CT.buffer(event_1b) 71 | 72 | self.assertEqual(2, len(self.CT.eventbuffer)) 73 | self.assertEqual(2, self.CT.eventbuffer[0].src_path) 74 | self.assertEqual(2, self.CT.eventbuffer[0].relevant_path) 75 | self.assertEqual(1, self.CT.eventbuffer[1].dest_path) 76 | self.assertEqual(1, self.CT.eventbuffer[1].relevant_path) 77 | 78 | @patch("barely.core.EventHandler.EventHandler") 79 | def test_empty_buffer(self, EH): 80 | def enbuffer(item): 81 | emptied_buffer.append(item) 82 | emptied_buffer = [] 83 | original_buffer = [1, 2, 3] 84 | 85 | self.CT.EH = EH 86 | self.CT.EH.notify = MagicMock(side_effect=enbuffer) 87 | self.CT.eventbuffer = original_buffer.copy() 88 | 89 | self.CT.empty_buffer() 90 | 91 | self.assertListEqual(original_buffer, emptied_buffer) 92 | self.assertListEqual([], self.CT.eventbuffer) 93 | 94 | @patch("signal.signal") 95 | @patch("multiprocessing.Process") 96 | @patch("watchdog.observers.Observer") 97 | def test_stop(self, observer, liveserver, signal): 98 | self.CT.observer = observer 99 | self.CT.observer.stop = MagicMock() 100 | self.CT.observer.join = MagicMock() 101 | self.CT.liveserver = liveserver 102 | self.CT.liveserver.join = MagicMock() 103 | self.CT.tracking = True 104 | self.CT.original_sigint = None 105 | 106 | self.CT.stop(None, None) 107 | 108 | self.assertTrue(self.CT.observer.stop.called) 109 | self.assertTrue(self.CT.observer.join.called) 110 | self.assertTrue(self.CT.liveserver.join.called) 111 | self.assertTrue(signal.called) 112 | self.assertFalse(self.CT.tracking) 113 | -------------------------------------------------------------------------------- /barely/tests/test_EventHandler.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from mock import patch 4 | from barely.common.config import config 5 | from barely.core.EventHandler import EventHandler 6 | from watchdog.events import FileCreatedEvent, FileModifiedEvent 7 | from watchdog.events import FileDeletedEvent, DirModifiedEvent 8 | from watchdog.events import FileMovedEvent, DirMovedEvent 9 | 10 | 11 | class TestEventHandler(unittest.TestCase): 12 | 13 | @classmethod 14 | def setUpClass(self): 15 | os.chdir("EventHandler") 16 | self.EH = EventHandler() 17 | 18 | @classmethod 19 | def tearDownClass(self): 20 | os.chdir("..") 21 | 22 | @patch("barely.core.ProcessingPipeline.init_jinja") 23 | def test_init_pipeline(self, jinja): 24 | self.EH.init_pipeline(1) 25 | self.assertTrue(jinja.called) 26 | 27 | @patch("barely.core.ProcessingPipeline.process") 28 | @patch("barely.core.EventHandler.EventHandler._determine_type") 29 | @patch("barely.core.ProcessingPipeline.move") 30 | @patch("barely.core.ProcessingPipeline.delete") 31 | @patch("barely.core.EventHandler.EventHandler._get_parent_page") 32 | @patch("barely.core.EventHandler.EventHandler._get_affected") 33 | @patch("barely.core.EventHandler.EventHandler._get_web_path") 34 | def test_notify(self, _get_web_path, _get_affected, _get_parent_page, delete, move, _determine_type, process): 35 | def join(*args): 36 | return os.path.join(*args) 37 | 38 | def catch_response(response): 39 | response_items.append(response[0]) 40 | response_items = [] 41 | 42 | # a bit counterintuitive, but easier to test: 43 | # only the origin ever changes with these mocks 44 | _get_web_path.return_value = "destination" 45 | _get_affected.side_effect = lambda x: [x[10:]] 46 | _get_parent_page.return_value = "parent" 47 | delete.return_value = None 48 | move.return_value = None 49 | _determine_type.return_value = ("TYPE", "ext") 50 | process.side_effect = catch_response 51 | 52 | golden_item = { 53 | "origin": "origin", 54 | "destination": "destination", 55 | "type": "TYPE", 56 | "extension": "ext" 57 | } 58 | 59 | # template modified 60 | golden_item["origin"] = "tpl" 61 | self.EH.notify(FileModifiedEvent(src_path=join("templates", "tpl"))) 62 | self.assertDictEqual(golden_item, response_items[-1]) 63 | 64 | # template moved 65 | golden_item["origin"] = "dest" 66 | self.EH.notify(FileMovedEvent(src_path=join("templates", "origin"), dest_path=join("templates", "dest"))) 67 | self.assertDictEqual(golden_item, response_items[-1]) 68 | 69 | # metadata.yaml 70 | golden_item["origin"] = "" 71 | self.EH.notify(FileModifiedEvent(src_path="metadata.yaml")) 72 | self.assertDictEqual(golden_item, response_items[-1]) 73 | 74 | # subpage 75 | golden_item["origin"] = "parent" 76 | self.EH.notify(FileModifiedEvent(src_path=join("", "_subpage", "origin.md"))) 77 | self.assertDictEqual(golden_item, response_items[-1]) 78 | 79 | # deleted 80 | self.EH.notify(FileDeletedEvent(src_path="trash")) 81 | self.assertTrue(delete.called) 82 | 83 | # moved 84 | _get_web_path.side_effect = lambda x: x 85 | self.EH.notify(DirMovedEvent(src_path="from", dest_path="to")) 86 | self.assertTrue(move.called) 87 | _get_web_path.side_effect = None 88 | 89 | # created file 90 | golden_item["origin"] = "file.png" 91 | self.EH.notify(FileCreatedEvent(src_path="file.png")) 92 | self.assertDictEqual(golden_item, response_items[-1]) 93 | 94 | # config.yaml, .git (other/pointless) 95 | self.EH.notify(FileModifiedEvent(src_path="config.yaml")) 96 | self.EH.notify(FileModifiedEvent(src_path=".git")) 97 | 98 | # DirModifiedEvent (other/pointless) 99 | self.EH.notify(DirModifiedEvent(src_path="dir")) 100 | 101 | # number of unique responses we should have received 102 | self.assertEqual(5, len(list({r["origin"]: r for r in response_items}.values()))) 103 | 104 | @patch("barely.core.EventHandler.EventHandler.notify") 105 | def test_force_rebuild(self, notify): 106 | notifications = [] 107 | 108 | def collect_notifications(notification): 109 | notifications.append(notification.src_path) 110 | 111 | os.chdir("force") 112 | 113 | notify.side_effect = collect_notifications 114 | self.EH.force_rebuild("devroot") 115 | 116 | golden_notified = { 117 | os.path.join(".", "dir", "_subpage", "subimage.jpg"), 118 | os.path.join(".", "dir", "page.md"), 119 | os.path.join(".", "dir", "image.png"), 120 | os.path.join(".", "file.txt") 121 | } 122 | self.assertSetEqual(golden_notified, set(notifications)) 123 | 124 | notifications = [] 125 | self.EH.force_rebuild("dir") 126 | golden_notified = { 127 | os.path.join(".", "dir", "_subpage", "subimage.jpg"), 128 | os.path.join(".", "dir", "page.md"), 129 | os.path.join(".", "dir", "image.png"), 130 | } 131 | self.assertSetEqual(golden_notified, set(notifications)) 132 | 133 | self.EH.force_rebuild(os.path.join(".", "dir")) 134 | self.assertSetEqual(golden_notified, set(notifications)) 135 | 136 | notifications = [] 137 | self.EH.force_rebuild("file.txt") 138 | golden_notified = { 139 | os.path.join(".", "file.txt") 140 | } 141 | self.assertSetEqual(golden_notified, set(notifications)) 142 | 143 | notifications = [] 144 | self.EH.force_rebuild("devroot", True) 145 | golden_notified = { 146 | os.path.join(".", "dir", "page.md"), 147 | os.path.join(".", "file.txt") 148 | } 149 | self.assertSetEqual(golden_notified, set(notifications)) 150 | 151 | os.chdir("..") 152 | 153 | @patch.dict(config, {"ROOT": {"DEV": "." + os.sep, "WEB": ""}, "PAGE_EXT": "md"}) 154 | @patch("barely.core.EventHandler.EventHandler._find_children") 155 | def test__get_affected(self, _f_c): 156 | def join(*args): 157 | return os.path.join("templates", *args) 158 | 159 | base = join("base.html") 160 | l_r_completelyalone = join("left", "right", "completelyalone.html") 161 | l_l = join("left", "left") 162 | 163 | os.chdir("affected") 164 | _f_c.return_value = [] 165 | self.assertSetEqual({"base.md"}, set(self.EH._get_affected(base))) 166 | self.assertSetEqual({"left.right.completelyalone.md"}, set(self.EH._get_affected(l_r_completelyalone))) 167 | self.assertSetEqual({"left.left.extendsbase.md", "left.left.extendschild.md"}, set(self.EH._get_affected(l_l))) 168 | 169 | os.chdir("..") 170 | 171 | def test__find_children(self): 172 | def join(*args): 173 | return os.path.join("templates", *args) + ".html" 174 | 175 | base = join("base") 176 | extendsdeeper = join("extendsdeeper") 177 | l_extendsbase = join("left", "extendsbase") 178 | l_parentless = join("left", "parentless") 179 | l_l_extendsbase = join("left", "left", "extendsbase") 180 | l_l_extendschild = join("left", "left", "extendschild") 181 | r_l_deeper = join("right", "left", "deeper") 182 | r_r_extendsparentless = join("right", "right", "extendsparentless") 183 | 184 | os.chdir("affected") 185 | 186 | golden_base = {l_extendsbase, l_l_extendsbase, l_l_extendschild} 187 | self.assertSetEqual(golden_base, set(self.EH._find_children(base))) 188 | 189 | golden_parentless = {l_extendsbase, r_r_extendsparentless, l_l_extendschild} 190 | self.assertSetEqual(golden_parentless, set(self.EH._find_children(l_parentless))) 191 | 192 | golden_childless = set() 193 | self.assertSetEqual(golden_childless, set(self.EH._find_children(extendsdeeper))) 194 | 195 | golden_reverse = {extendsdeeper} 196 | self.assertSetEqual(golden_reverse, set(self.EH._find_children(r_l_deeper))) 197 | 198 | golden_once = {l_l_extendschild} 199 | self.assertSetEqual(golden_once, set(self.EH._find_children(l_extendsbase))) 200 | 201 | os.chdir("..") 202 | 203 | def test__determine_type(self): 204 | os.chdir("type") 205 | self.assertTupleEqual(("PAGE", "md"), self.EH._determine_type("file.md")) 206 | self.assertTupleEqual(("IMAGE", "png"), self.EH._determine_type("file.png")) 207 | self.assertTupleEqual(("TEXT", "css"), self.EH._determine_type("file.css")) 208 | self.assertTupleEqual(("GENERIC", "mp4"), self.EH._determine_type("binary.mp4")) 209 | self.assertTupleEqual(("GENERIC", "NOTYPE"), self.EH._determine_type("notype")) 210 | os.chdir("..") 211 | 212 | @patch.dict(config, {"PAGE_EXT": "md", "ROOT": {"WEB": "web", "DEV": "dev"}}) 213 | def test__get_web_path(self): 214 | devroot = config["ROOT"]["DEV"] 215 | webroot = config["ROOT"]["WEB"] 216 | 217 | def join(*args): 218 | return os.path.join(*args) 219 | 220 | def get(file): 221 | return self.EH._get_web_path(join(devroot, file)) 222 | 223 | self.assertEqual(join(webroot, "index.html"), get("test.md")) 224 | self.assertEqual(join(webroot, "generic.txt"), get("generic.txt")) 225 | self.assertEqual(join(webroot, "noext"), get("noext")) 226 | 227 | def test__get_parent_page(self): 228 | os.chdir("getparent") 229 | parent = self.EH._get_parent_page(os.path.join("_sub", "child.md")) 230 | self.assertEqual("parent.md", parent) 231 | 232 | parent = self.EH._get_parent_page("_sub") 233 | self.assertEqual("parent.md", parent) 234 | 235 | with self.assertRaises(IndexError) as context: 236 | self.EH._get_parent_page(os.path.join("none", "none", "parentless.md")) 237 | self.assertTrue("Child page has no parent!" in str(context.exception)) 238 | os.chdir("..") 239 | -------------------------------------------------------------------------------- /barely/tests/test_PluginManager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from mock import patch 4 | from unittest.mock import MagicMock 5 | from barely.plugins import PluginBase 6 | from barely.common.config import config 7 | from barely.plugins.PluginManager import PluginManager 8 | 9 | 10 | class SamplePlugin(PluginBase): 11 | def action(self, item): 12 | yield item 13 | 14 | def register(self): 15 | return "Base", -1, [] 16 | 17 | 18 | class TestPluginBase(unittest.TestCase): 19 | 20 | def test___init__(self): 21 | mock_config = {} 22 | with patch.dict(config, mock_config): 23 | SP = SamplePlugin() 24 | self.assertIsNotNone(SP.config) 25 | 26 | def test_register(self): 27 | plugin = SamplePlugin() 28 | registration_info = plugin.register() 29 | self.assertTupleEqual(("Base", -1, []), registration_info) 30 | 31 | def test_action(self): 32 | plugin = SamplePlugin() 33 | golden_item = { 34 | "test": "dict" 35 | } 36 | 37 | test_item = plugin.action(item=golden_item) 38 | 39 | self.assertDictEqual(golden_item, list(test_item)[0]) 40 | 41 | 42 | class TestPluginManager(unittest.TestCase): 43 | 44 | @classmethod 45 | def setUpClass(self): 46 | with patch.object(PluginManager, "__init__", lambda x: None): 47 | self.PM = PluginManager() 48 | 49 | os.chdir("PluginManager") 50 | 51 | @classmethod 52 | def tearDownClass(self): 53 | os.chdir("..") 54 | 55 | @patch("barely.plugins.PluginManager.PluginManager.discover_plugins") 56 | def test___init__(self, discover): 57 | mock_config = { 58 | "PLUGIN_PATHS": { 59 | "SYS": { 60 | "CONTENT": "", 61 | "BACKUP": "", 62 | "PUBLICATION": "" 63 | }, 64 | "USER": { 65 | "CONTENT": "", 66 | "BACKUP": "", 67 | "PUBLICATION": "" 68 | } 69 | } 70 | } 71 | with patch.dict(config, mock_config): 72 | PM = PluginManager() 73 | self.assertTrue(discover.called) 74 | self.assertIsNotNone(PM.plugins_content) 75 | self.assertIsNotNone(PM.plugins_backup) 76 | self.assertIsNotNone(PM.plugins_publication) 77 | 78 | def test_discover_plugins(self): 79 | # Content Plugins register with filetypes and a priority 80 | test_dict = self.PM.discover_plugins(["content", "empty"]) 81 | 82 | self.assertEqual(3, len(test_dict)) 83 | self.assertIn("md", test_dict) 84 | self.assertIn("pdf", test_dict) 85 | self.assertIn("png", test_dict) 86 | 87 | self.assertEqual(1, len(test_dict["md"])) 88 | self.assertTrue(issubclass(type(test_dict["md"][0]), PluginBase)) 89 | 90 | self.assertEqual(2, len(test_dict["png"])) 91 | self.assertTrue(issubclass(type(test_dict["png"][0]), PluginBase)) 92 | self.assertTrue(issubclass(type(test_dict["png"][1]), PluginBase)) 93 | 94 | self.assertTrue(test_dict["png"][0].register()[0] == "P2") 95 | self.assertTrue(test_dict["png"][1].register()[0] == "P1") 96 | 97 | self.assertEqual(1, len(test_dict["pdf"])) 98 | self.assertTrue(issubclass(type(test_dict["pdf"][0]), PluginBase)) 99 | 100 | # Backup/Publication Plugins only register with their class 101 | test_list = self.PM.discover_plugins(["other"], type_content=False) 102 | self.assertEqual(2, len(test_list)) 103 | self.assertTrue(issubclass(type(test_list[0]), PluginBase)) 104 | self.assertTrue(issubclass(type(test_list[1]), PluginBase)) 105 | self.assertTrue(test_list[0].register()[0] == "P4") 106 | self.assertTrue(test_list[1].register()[0] == "P3") 107 | 108 | def test_hook_content(self): 109 | # Sample Item, to be processed by plugins registered for the "test" extension 110 | item = { 111 | "extension": "test" 112 | } 113 | 114 | # The first sample plugin returns 2 items for every input item, merely duplicating it 115 | sample_plugin1 = SamplePlugin() 116 | sample_plugin1.action = MagicMock(return_value=[item, item]) 117 | 118 | # The second plugin is the constant 1-function 119 | sample_plugin2 = SamplePlugin() 120 | sample_plugin2.action = MagicMock(return_value=1) 121 | 122 | # Both plugins are "registered" for the test extension 123 | self.PM.plugins_content = { 124 | "test": [sample_plugin1, sample_plugin2] 125 | } 126 | 127 | # The hook should return two "1"s. 128 | returned_content = self.PM.hook_content(item) 129 | self.assertEqual([1, 1], returned_content) 130 | 131 | def test_hook_backup(self): 132 | sample_plugin = SamplePlugin() 133 | sample_plugin.action = MagicMock() 134 | 135 | self.PM.plugins_backup = [sample_plugin] 136 | 137 | self.assertFalse(sample_plugin.action.called) 138 | self.PM.hook_backup() 139 | self.assertTrue(sample_plugin.action.called) 140 | 141 | def test_hook_publication(self): 142 | sample_plugin1 = SamplePlugin() 143 | sample_plugin1.action = MagicMock() 144 | 145 | sample_plugin2 = SamplePlugin() 146 | sample_plugin2.action = MagicMock() 147 | 148 | self.PM.plugins_publication = [sample_plugin1, sample_plugin2] 149 | 150 | self.assertFalse(sample_plugin1.action.called) 151 | self.assertFalse(sample_plugin2.action.called) 152 | self.PM.hook_publication() 153 | self.assertTrue(sample_plugin1.action.called) 154 | self.assertTrue(sample_plugin2.action.called) 155 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to the barely docs! 2 | 3 | 1. [About](about.md) 4 | 2. [Getting started](getting-started.md) 5 | 3. [Detailed Overview](detailed-overview.md) 6 | 4. [Modular Pages](modular-pages.md) 7 | 5. [Plugins](plugins.md) 8 | - [Writing your own Plugins](plugins.md#writing-your-own-plugins) 9 | 6. [Blueprints](blueprints.md) 10 | 7. [Lighthouse](lighthouse.md) 11 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # About barely 2 | 3 | barely was built out of frustration with the readily available site generators, frameworks and CMS, which mostly fall into two categories: not providing crucial features; or providing such an overload of them that gettig started with the system takes longer than just building the site by hand. 4 | 5 | barely won't be the right tool for everyone and every project, and neither does it try to be. But it might be the right tool for your project, if: 6 | 7 | * you want to build a static webpage 8 | * you want to do so rapidly, with the barest minimum of setup and configuration 9 | * you value live reloading of every one of your changes, including SCSS/SASS, images and templates 10 | * you are satisfied with the featureset expected of a typical website or blog 11 | 12 | In those circumstances, barely aims to give you as smooth an experience as possible, by following these design principles: 13 | 14 | ## Simplicity 15 | 16 | All your files live in one directory (your devroot). You have - at most - two config files, one for configuring barelys behaviour, one for global metadata. You don't have to touch either one if you don't want to. 17 | 18 | barely renders markdown content and jinja2 templates into HTML pages. That's it. (OK, that's only it if you deactivate all the awesome [plugins](plugins.md) barely ships with.) 19 | 20 | ## Workflow 21 | 22 | If you start barely by typing `barely live` (or just `barely`), a live server starts and opens your project in your preferred browser. Any changes you save - be it in a page file, its yaml configuration, a template or even CSS/JS/SASS/... get reflected immediately. This makes working a breeze. 23 | 24 | When you've finished, simply hit `Ctrl+C`, and press enter on barelys prompt to push your changes to git, publish the site to your sftp server, or any other action you've specified. 25 | 26 | Since building performant and SEO-friendly websites is always important, barely comes bundled with a Google **Lighthouse** CLI option, letting you quickly generate reports about your sites health. 27 | 28 | ## Extensibility 29 | 30 | barely comes with 10 [plugins](plugins.md) that make working even easier, like automatically compressing images, compiling SASS, generating complete HTML forms out of yaml, and managing Collections (barelys catch-all term for things like tags or categories of posts and pages). 31 | 32 | Should you still miss some functionality, chances are you can implement it in minutes, thanks to barelys super simple [plugins API](plugins.md#writing-your-own-plugins). 33 | 34 | [< back](README.md) 35 | -------------------------------------------------------------------------------- /docs/barely-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/docs/barely-demo.gif -------------------------------------------------------------------------------- /docs/blueprints.md: -------------------------------------------------------------------------------- 1 | # Blueprints 2 | 3 | Back in the [Basics](/README.md/#basics), we have already briefly covered blueprints. They are pretty much exactly what you would expect: re-usable project templates that you can instantiate into new projects. Other frameworks might call them themes. 4 | 5 | You can list all available blueprints with: 6 | ```console 7 | $ barely blueprints 8 | [barely][ core][ INFO] :: found 2 blueprints: 9 | -> default 10 | -> blank 11 | ``` 12 | 13 | The help menu hints at a way to also create your own blueprints: 14 | ```console 15 | $ barely blueprints --help 16 | Usage: barely blueprints [OPTIONS] 17 | 18 | list all available blueprints, or create a new one 19 | 20 | Options: 21 | -n, --new TEXT create a reusable blueprint from the current project 22 | --help Show this message and exit. 23 | ``` 24 | 25 | Executing `barely blueprints --new "name"` will create a new blueprint out of your current project, and you can freely use it from now on: 26 | 27 | ```console 28 | $ barely blueprints 29 | [barely][ core][ INFO] :: found 3 blueprints: 30 | -> name 31 | -> default 32 | -> blank 33 | ``` 34 | 35 | [< back](README.md) 36 | -------------------------------------------------------------------------------- /docs/detailed-overview.md: -------------------------------------------------------------------------------- 1 | # Detailed Overview 2 | 3 | There are a couple of things that are important to know about how barely works. If you've used similar frameworks before, you'll probably already be familiar with most of these things. barely doesn't try to reinvent the wheel. 4 | 5 | 1. Templates: 6 | 7 | - all your templates live in the `templates/` folder inside your devroot. If, for whatever reason, you want to place them somewhere else, set the `TEMPLATES_DIR` variable in your `config.yaml`. 8 | 9 | - you can organize your templates freely inside that folder, including the use of subfolders. 10 | 11 | - you set a template for a page by naming the pages markdown file according to this scheme: 12 | 13 | - you have the following template: `templates/something.html`. To use it, name any markdown file `something.md`. 14 | 15 | - if your template lives in a subfolder, you specify it with a `.`: to use `templates/subdir/other.html`, name your markdown file `subdir.other.md`. 16 | 17 | - templates can include or extend other templates: 18 | 19 | - `{% include "subdir/other.html" %}` 20 | 21 | - `{% extends "something.html" %}` 22 | 23 | - for any additional information regarding templates, please refer to the [official jinja2 documentation](https://jinja.palletsprojects.com/en/3.0.x/). 24 | 25 | 2. YAML & Markdown 26 | 27 | If you are not familiar with Markdown yet, GitHub has an [excellent guide](https://guides.github.com/features/mastering-markdown/) on it. You will create a Markdown file corresponding to every page of your website. 28 | 29 | Inside of every markdown file, you can specify variables to be used either in some plugin, or in your templates. To do so, the first line of the file has to be `---`, and the same delimiter has to be used before any markdown contents. 30 | 31 | Inbetween the delimiters, you can use normal YAML syntax: 32 | ```yaml 33 | --- 34 | title: "Page title for use in a template!" 35 | description: "..." 36 | nested: 37 | - value 38 | - something else 39 | --- 40 | ``` 41 | These variables can be used like any others in your templates: `{{ title }}`. 42 | 43 | Sometimes, plugins expect (or allow) for configuration on a page level (e.g. the [Highlight](plugins/highlight.md) plugin). Additionally, barely pays attention to two page level configurations: 44 | ```yaml 45 | extension: specify a extension different from html (without a leading dot). E.g.: php, xml,... 46 | publish: if set to false, this page will not be rendered. Useful for drafting. 47 | ``` 48 | 49 | Of course, both of these may also be set at a global level. 50 | 51 | Both the initial YAML section and any Markdown are optional. If you want to, your file can be completely empty. In that case, the template specified by the filename will still get rendered as usual. 52 | 53 | 54 | 3. Configuration Files 55 | 56 | You can utilize two configuration files: 57 | - `config.yaml`: configure barely's behaviour. You have to at least specify the paths to your webroot and devroot, like this: 58 | ```yaml 59 | ROOT: 60 | DEV: /[...]/devroot 61 | WEB: /[...]/webroot 62 | ``` 63 | 64 | barely also sets some standard values which you can optionally override: 65 | ```yaml 66 | TEMPLATES_DIR: templates 67 | PAGE_EXT: md 68 | IMAGE_EXT: 69 | - jpg 70 | - jpeg 71 | - png 72 | IGNORE: 73 | - .git 74 | ``` 75 | 76 | **Note:** `config.yaml` is also the place for [plugin configurations](plugins.md) 77 | 78 | - `metadata.yaml`: set global variables. You can leave this file empty or completely remove it. 79 | 80 | 4. Other Files 81 | 82 | Any other files will get copied over into your webroot (possibly after being processed by your enabled plugins), as long as they aren't set to be ignored in your `config.yaml`. 83 | 84 | ## Modular Pages 85 | 86 | See the [Modular Pages](modular-pages.md) page. 87 | 88 | ## Plugins 89 | 90 | See the [Plugins](plugins.md) page. 91 | 92 | ## Blueprints 93 | 94 | See the [Blueprints](blueprints.md) page. 95 | 96 | [< back](README.md) 97 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Good news: Getting started with barely is super easy! This guide elaborates on the [Quickstart Guide](/README.md#quickstart) in the README. 4 | 5 | ### Prerequisites 6 | 7 | Make sure you have python >= 3.9 installed: 8 | ```console 9 | $ python -V 10 | Python 3.9.x 11 | ``` 12 | 13 | (On Windows: `py -V`) 14 | 15 | It is highly recommended to create a virtual environment for barely, otherwise some parts may not work: 16 | ```console 17 | $ python -m venv .venv 18 | $ . .venv/bin/activate 19 | (.venv) $ 20 | ``` 21 | 22 | (On Windows: `py -m venv .venv` and `.venv\Scripts\activate`) 23 | 24 | ### Installation 25 | 26 | Now, simply install barely like any other package: 27 | ```console 28 | (.venv) $ pip install barely 29 | ``` 30 | 31 | (On Windows: `py -m pip install barely`) 32 | 33 | That's it! Congrats! 34 | 35 | 36 | ## Usage Basics 37 | 38 | Now let's get familiar with using barely! 39 | 40 | 1. Type `barely --help` to get an overview over available commands and options: 41 | ```console 42 | $ barely --help 43 | Usage: barely [OPTIONS] COMMAND [ARGS]... 44 | 45 | barely reduces static website development to its key parts, by automatically 46 | rendering jinja2 templates and Markdown content into HTML. A simple plugin 47 | interface allows for easy extensibility, and the built-in live web server 48 | makes on-the-fly development as comfortable as possible. 49 | 50 | Options: 51 | -d, --debug set logging level to debug 52 | --help Show this message and exit. 53 | 54 | Commands: 55 | live* starts a live server, opens your project in the browser and... 56 | blueprints list all available blueprints, or create a new one 57 | lighthouse use Google Lighthouse to evaluate a page for SEO- and... 58 | new create a new barely project (optionally with a blueprint) 59 | rebuild (re)build the entire project 60 | test run the testsuite to verify the install 61 | ``` 62 | 63 | 2. Try typing `barely`, `barely live`, or `barely rebuild`: 64 | ```console 65 | $ barely 66 | [barely][ core][ERROR] :: could not find 'config.yaml'. Exiting 67 | ``` 68 | What happened? barely is telling us that we aren't currently in a barely project directory. For a directory to count as a project, it has to contain a `config.yaml` file, which in turn has to specify the devroot (where we will work) and the webroot (where barely renders to). 69 | 70 | So lets change that! 71 | 72 | 3. Create a new project with `barely new`: 73 | ```console 74 | $ barely new 75 | [barely][ core][ INFO] :: setting up new project with parameters: 76 | -> webroot: webroot 77 | -> devroot: devroot 78 | -> blueprint: default 79 | [barely][ core][ INFO] :: setting up basic config... 80 | [barely][ core][ INFO] :: done. 81 | ``` 82 | Sweet! barely created two new subdirectories, `devroot` and `webroot`. The project was also created with a blueprint, namely `default`, which is why our `devroot` is not empty. We will learn about blueprints in a second. 83 | 84 | BTW: you can easily change the project creation parameters, see for reference: 85 | ```console 86 | $ barely new --help 87 | Usage: barely new [OPTIONS] 88 | 89 | create a new barely project (optionally with a blueprint) 90 | 91 | Options: 92 | -b, --blueprint TEXT instantiate project from a blueprint 93 | -w, --webroot TEXT location for the generated static files 94 | -d, --devroot TEXT project directory, for development files 95 | --help Show this message and exit. 96 | ``` 97 | 98 | 4. Let's have a look around! 99 | ```console 100 | $ cd devroot 101 | $ tree . 102 | . 103 | ├── config.yaml 104 | ├── metadata.yaml 105 | ├── template.md 106 | └── templates 107 | ├── template2.html 108 | └── template.html 109 | 110 | 1 directory, 5 files 111 | ``` 112 | - `config.yaml` contains all the configuration for barely and its plugins. Right now, it only contains the absolute paths of the devroot and webroot 113 | - `metadata.yaml` is a place you can put any values you want to use in multiple places across your project, be it metadata or any other variables 114 | - `template.md` is the Markdown file for the root page of the website. Its contents will get rendered into `webroot/index.html` with the `templates/template.md` template 115 | - `templates/` contains all your templates 116 | 117 | 6. Let's build the project! 118 | ```console 119 | $ barely rebuild 120 | [barely][ core][ INFO] :: registering plugins... 121 | [barely][ core][ INFO] :: 7 plugins registered. 122 | [barely][ core][ INFO] :: rebuilding devroot... 123 | -> deleted /[...]/webroot 124 | [barely][ core][ INFO] :: event at /[...]/devroot/template.md 125 | -> rendered, highlighted /[...]/devroot/template.md -> /[...]/webroot/index.html 126 | [barely][ core][ INFO] :: rebuild complete. 127 | [barely][ core][ INFO] :: Finalizing plugin ReadingTime... 128 | [barely][ core][ INFO] :: Finalizing plugin ToC... 129 | [barely][ core][ INFO] :: Finalizing plugin AutoSEO... 130 | [barely][ core][ INFO] :: Finalizing plugin Highlight... 131 | [barely][ core][ INFO] :: Finalizing plugin Forms... 132 | [barely][ core][ INFO] :: Finalizing plugin Minify... 133 | [barely][ core][ INFO] :: Finalizing plugin Gallery... 134 | [barely][ core][ INFO] :: .. 135 | -> Do you want to Publish / Backup / do both? 136 | -> *[n]othing | [p]ublish | [b]ackup | [Y]do both :: n 137 | [barely][ core][ INFO] :: exited. 138 | ``` 139 | 140 | And then start the live server: 141 | ```console 142 | $ barely 143 | [barely][ core][ INFO] :: registering plugins... 144 | [barely][ core][ INFO] :: 7 plugins registered. 145 | [barely][ core][ INFO] :: started tracking... 146 | ``` 147 | 148 | We could also have combined those two steps with the `-s` flag like this: `barely rebuild -s`, to start the live server immediately after rebuilding. 149 | 150 | Your favorite browser should open, and you will be greeted with the rendered version of `template.md`. 151 | 152 | Now is a good time to play around a bit with your sample project - make some changes to the contents, the templates or add a stylesheet and watch the page update in real time! 153 | 154 | When you feel comfortable with the workings of barely, move on to the [next section](detailed-overview.md). 155 | 156 | [< back](README.md) 157 | -------------------------------------------------------------------------------- /docs/lighthouse.md: -------------------------------------------------------------------------------- 1 | # Lighthouse 2 | 3 | Google Lighthouse is an open-source project that can generate highly insightful metrics about a single page of your website. These metrics contain scores about the SEO-friendliness of your site, its accessibility, and much more. 4 | 5 | [Learn more about it here.](https://developers.google.com/web/tools/lighthouse) 6 | 7 | There is no native python implementation for Lighthouse, rather, if you want to use it, you will have to have the following installed: 8 | 9 | - node 10 | - [lighthouse](https://www.npmjs.com/package/lighthouse) (npm package) 11 | - Chrome or Chromium 12 | 13 | These are **not** dependencies of barely. You only have to install these if you want to use Lighthouse. 14 | 15 | To generate a report of your root page: 16 | ```console 17 | $ barely lighthouse 18 | [barely][ core][ INFO] :: Starting evaluation using lighthouse 8.3.0... 19 | [barely][ core][ INFO] :: Finished the evaluation! Opening the result now. 20 | ``` 21 | 22 | You can also specify any other page to be evaluated, or force the evaluation of the desktop version of a page: 23 | ```console 24 | $ barely lighthouse --help 25 | Usage: barely lighthouse [OPTIONS] 26 | 27 | use Google Lighthouse to evaluate a page for SEO- and accessibility scores 28 | 29 | Options: 30 | -d, --desktop evaluate full-width page. default is mobile. 31 | -p, --page TEXT specify a page to be evaluated other than the root 32 | --help Show this message and exit. 33 | ``` 34 | 35 | [< back](README.md) 36 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charludo/barely/4bddbdd414ddd8524d6fb7daffbafbb3383b4619/docs/logo.png -------------------------------------------------------------------------------- /docs/modular-pages.md: -------------------------------------------------------------------------------- 1 | # Modular Pages 2 | 3 | Sometimes, a page can contain many different sections, all with their own content, structure and design. An example of this are websites in the OnePage style, or with a long landing page. You might have distinct sections, like "About", "Services", and "Contact" on one page. Since there isn't really a way to split the contents of one pages markdown (it will all get rendered into a single `
    `), barely features modular pages. 4 | 5 | These differ from normal pages in that they won't get rendered on their own, and thus are not linkable in a menu. Instead, they get "absorbed" into the parent page. 6 | 7 | ## Defining a modular page 8 | 9 | To define a modular page, simply put the "modular" argument into that pages configuration: 10 | ```yaml 11 | --- 12 | title: My Parent Page 13 | modular: 14 | - about 15 | - services 16 | - contact 17 | --- 18 | ``` 19 | 20 | Here, we told barely that our parent page has three children: about, services, and contact. For each of those, barely expects a folder with that exact name, lead by an `underscore`. 21 | Inside those folders, you can define pages and their templates, as well as any additional ressources, just like you would with any normal page. 22 | 23 | Notably, the names in our `modular` variable only reflect what the child pages are called, **not** what template they use. So our directory could (for example) look like this, 24 | ```console 25 | $ tree . 26 | . 27 | ├── _about 28 | │   └── child.md 29 | ├── _contact 30 | │   └── contactform.md 31 | ├── parent.md 32 | └── _services 33 | └── child.md 34 | ``` 35 | where _about and _services share the same template, but _contact does not. 36 | 37 | Also note that subpages get handed to plugins just like any other pages! They can not, however, have subpages of their own. 38 | 39 | To display the subpages, the template for your parent.md has to look somewhat like this: 40 | ```html 41 | {% for sp in sub_pages %} 42 | {{ sp }} 43 | {% endfor %} 44 | ``` 45 | 46 | You can not access subpages by their name, `sub_pages` is a list containing the rendered subpages, ordered the same way that you specified them in back in the parents YAML. 47 | 48 | ## Organization 49 | 50 | It's good style to keep your modular templates somewhat separate from your normal ones. This is as easy as creating a `modular/` folder inside your `templates/` directory, then naming the subpages like `modular.child.md`. 51 | 52 | [< back](README.md) 53 | -------------------------------------------------------------------------------- /docs/plugins/autoseo.md: -------------------------------------------------------------------------------- 1 | # AutoSEO 2 | 3 | Type: `content` 4 | Enabled by default: `true` 5 | 6 | Generate all the pesky meta-tags from a very limited configuration. This plugin can save you a lot of time, and make your pages much more appealing to Google & Co. 7 | 8 | AutoSEO is quite smart about figuring out good values for tags, and has multiple fallback sources for every one of them. Most of the time you will get away with a very basic config (see below). 9 | 10 | Set up your `metadata.yaml` like this: 11 | 12 | ```yaml 13 | site_url: the URL at which you plan on publishing your website 14 | site_name: name of your website 15 | site_description: optional (fallback) if no page-specific description is found 16 | site_keywords: keywords befitting to all pages 17 | favicon: favicon 18 | twitter_site: your twitter profile 19 | twitter_creator: what twitter user to attribute content to. better specified on a per-page-basis. 20 | ``` 21 | 22 | **ALL** of these are optional. 23 | 24 | Then make sure you have a `title` specified in the meta of your pages (`site_name` will be used as fallback). Either provide a summary, or a description. 25 | 26 | You can specify a title_image, if none is found, the plugin will extract one from your markdown; if none is found again, it will look for one in the page's directory in the devroot. 27 | 28 | You can also override these values specifically for social media (OpenGraph/Facebook and Twitter). 29 | 30 | All in all, in addition to the above global values, these attributes are respected: 31 | 32 | - title 33 | - title_image 34 | - description 35 | - summary 36 | - keywords 37 | - robots 38 | - SEO: 39 | - title 40 | - title_image 41 | - title_image_alt 42 | - description 43 | - site_name 44 | - twitter_card 45 | - twitter_site 46 | - twitter_creator 47 | --- 48 | 49 | **Note:** autoSEO ignores modular subpages - its configuration has to happen within the parent's yaml config. 50 | 51 | Use the generated tags in the `` of your base template: 52 | 53 | ```HTML 54 | 55 | 56 | {{ seo_tags }} 57 | ... 58 | 59 | ... 60 | 61 | ``` 62 | 63 | If you have not manually created a `robots.txt` and/or `sitemap.txt` already, the plugin will do it for you. 64 | 65 | --- 66 | **config.yaml key:** AUTO_SEO 67 | 68 | |argument |default value |explanation | 69 | |-----------------------|-------------------|-----------------------------------------------| 70 | |PRIORITY |30 | | 71 | |MISC_TAGS |true |auto-generate charset & viewport tags | 72 | -------------------------------------------------------------------------------- /docs/plugins/collections.md: -------------------------------------------------------------------------------- 1 | # Collections 2 | 3 | Type: `content` 4 | Enabled by default: `false` 5 | 6 | Collections allow you to include previews of other pages on a page, or to add your page to a collection. 7 | **Note**: might not update properly during live mode, but will catch up when quitting barely. 8 | 9 | 10 | 11 | --- 12 | 13 | Add a page to collections like this: 14 | ```yaml 15 | collections: 16 | - col1 17 | - col2 18 | ``` 19 | 20 | Exhibit pages belonging to collections like this: 21 | ```yaml 22 | exhibits: 23 | - col3 24 | - col4 25 | ``` 26 | 27 | You can then display exhibits in your templates: 28 | ```html 29 | {% for exhibit in exhibits %} 30 | {% for collectible in exhibit %} 31 | {{ collectible["title"] }} 32 | {{ collectible["preview"] }} 33 | {{ collectible["href"] }} 34 | {% endfor %} 35 | {% endfor %} 36 | ``` 37 | 38 | Exhibits contain the following tags / information: 39 | ```yaml 40 | title: title of the exhibit 41 | preview: the first few words of the page 42 | raw: the entire markdown content of the page 43 | href: link to the page 44 | image: link to image, if title_image was specified on the original page 45 | date: modification or creation time of the page 46 | reading_time: reading time as provided by ReadingTime plugin 47 | ``` 48 | 49 | Inside the OVERVIEW_TEMPLATE, list the available collections like this: 50 | ```html 51 | {% for collection in collections_list %} 52 | {{ collection["name"] }} 53 | {{ collection["size"] }} 54 | {{ collection['href'] }} 55 | {% endfor %} 56 | ``` 57 | where the `size` is the number of collectibles in the collection. 58 | 59 | --- 60 | **config.yaml key:** COLLECTIONS 61 | |argument |default value |explanation | 62 | |-------------------|-------------------|-----------------------------------------------------------------------------------------| 63 | |PRIORITY |999 | | 64 | |PAGE |categories |where collection overview pages get rendered to | 65 | |SUMMARY_LENGTH |100 |cut-off for summaries of pages / posts | 66 | |OVERVIEW_TITLE | |title of the collections overview page | 67 | |OVERVIEW_TEMPLATE | |template for the collections overview | 68 | |OVERVIEW_CONTENT | |points to markdown file containingcontent of OVERVIEW page | 69 | |COLLECTION_TEMPLATE| |template for the individual collection pages | 70 | |ORDER_KEY |timestamp |specify a key by which collectibles are sorted on collection pages and within exhibitions| 71 | |ORDER_REVERSE |true |reverses the sorting order | 72 | -------------------------------------------------------------------------------- /docs/plugins/forms.md: -------------------------------------------------------------------------------- 1 | # Forms 2 | 3 | Type: `content` 4 | Enabled by default: `true` 5 | 6 | Specify forms in YAML syntax! 7 | 8 | --- 9 | 10 | A sample form could look like this: 11 | ```yaml 12 | forms: 13 | example: 14 | action: sample.php 15 | classes: large 16 | 17 | group-basic: 18 | legend: Basic Data 19 | name: 20 | type: text 21 | required: true 22 | placeholder: Name 23 | mail: 24 | type: email 25 | required: true 26 | placeholder: E-Mail 27 | 28 | topic: 29 | type: select 30 | multiple: false 31 | default: standard 32 | options: 33 | n1: something 34 | n2: standard 35 | n3: something else 36 | 37 | message: 38 | type: textarea 39 | required: true 40 | classes: extra 41 | value: Pre-filled with this 42 | 43 | gdpr: 44 | type: checkbox 45 | value: agreed 46 | label: Agree to GDPR 47 | name: gdpr-checkbox 48 | 49 | send: 50 | type: button 51 | value: Send Now 52 | action: submit 53 | ``` 54 | 55 | You can then display the form in your templates: 56 | ```html 57 | {{ form_example }} 58 | ``` 59 | 60 | Forms can contain the following attributes: 61 | ```yaml 62 | classes: extra classes 63 | action: what to to do on submit 64 | ``` 65 | 66 | Groups can have the following attributes: 67 | ```yaml 68 | classes: extra classes 69 | legend: a legend for the fieldset 70 | ``` 71 | 72 | Form fields can have the following attributes: 73 | ```yaml 74 | classes: extra classes 75 | required: bool 76 | label: false or label text 77 | label-after: false or label text 78 | type: type of input 79 | options: needed by select, radio types 80 | default: default value 81 | multiple: bool (for select) 82 | placeholder: placeholder value 83 | rows: for textarea 84 | cols: for textarea 85 | ``` 86 | --- 87 | **config.yaml key:** FORMS 88 | |argument |default value |explanation | 89 | |-------------------|-------------------|-----------------------------------------------| 90 | |PRIORITY |5 | | 91 | -------------------------------------------------------------------------------- /docs/plugins/gallery.md: -------------------------------------------------------------------------------- 1 | # Gallery 2 | 3 | Type: `content` 4 | Enabled by default: `true` 5 | 6 | Turn simple markdown into a list of images (aka, a gallery). 7 | 8 | Default priority is lower than that of Pixelizer, meaning the Gallery will receive the full optimization benefits of Pixelizer. 9 | 10 | --- 11 | 12 | Markdown-Syntax: 13 | 14 | ```md 15 | [name ]!!(image-folder) 16 | ``` 17 | 18 | The name of the gallery and the image folder are required. Sorting key and direction are optional; however, it is not possible to specify `direction` without `sort`, since these are positional arguments. 19 | 20 | Each image will receive an `alt` tag consisting of the gallery name, and the image's position within the gallery. 21 | 22 | --- 23 | **config.yaml key:** GALLERY 24 | |argument |default value |explanation | 25 | |-------------------|-------------------|-----------------------------------------------| 26 | |PRIORITY |2 | | 27 | |DEFAULT_SORT |name |name or time | 28 | |DEFAULT_DIRECTION |asc |asc or desc | 29 | |GALLERY_CLASS |gallery |class for the wrapping `div` | 30 | 31 | The wrapping `
    ` also gets an ID of `gallery-{name}`. 32 | -------------------------------------------------------------------------------- /docs/plugins/git.md: -------------------------------------------------------------------------------- 1 | # Git 2 | 3 | Type: `backup` 4 | Enabled by default: `true` 5 | 6 | Pushes to a remote origin whenever you are done rebuilding / live editing (unless you answer barelys exit prompt wih "no" or "publish only"). 7 | 8 | **Note:** git has to be installed. On Windows, GitHub Desktop is **not** sufficient. Check with `git --version`. 9 | 10 | **Note**: the repository and its origin have to already be initialized. This plugin can not do that for you. 11 | 12 | --- 13 | **config.yaml key:** GIT 14 | |argument |default value |explanation | 15 | |-----------------------|-------------------|-----------------------------------------------| 16 | |PRIORITY |40 | | 17 | |MESSAGE |barely auto commit |gets appended with a timestamp | 18 | |REMOTE_NAME |origin | | 19 | -------------------------------------------------------------------------------- /docs/plugins/highlight.md: -------------------------------------------------------------------------------- 1 | # Highlight 2 | 3 | Type: `content` 4 | Enabled by default: `true` 5 | 6 | Lex and highlight `markdown code blocks`. You can configure a global theme and lexer, and then override both in page configs; you can also specify the lexer on a per-code-block-level like this: 7 | 8 | ```python 9 | 10 | 11 | 12 | ``` 13 | 14 | --- 15 | 16 | For a full list of features, check [pygments.org](https://pygments.org/)! 17 | 18 | --- 19 | **config.yaml key:** HIGHLIGHT 20 | |argument |default value |explanation | 21 | |-------------------|-------------------|-----------------------------------------------| 22 | |PRIORITY |20 | | 23 | |CLASS_PREFIX |hl |in case of conflicting styles | 24 | |LINE_NOS |table | | 25 | |TABSIZE |4 | | 26 | |ENCODING |utf-8 | | 27 | |THEME |default |check pygments.org for a list | 28 | |LEXER | |empty means guessing | 29 | |ASSETS_DIR |assets |where to place theme css files | 30 | -------------------------------------------------------------------------------- /docs/plugins/localbackup.md: -------------------------------------------------------------------------------- 1 | # Local Backup 2 | 3 | Type: `backup` 4 | Enabled by default: `false` 5 | 6 | Keeps a limited number of project copies on your local machine. 7 | 8 | **Please use the Git plugin for actual backups!** 9 | 10 | Restoring a backup is a manual task. 11 | 12 | --- 13 | **config.yaml key:** LOCAL_BACKUP 14 | |argument |default value |explanation | 15 | |-----------------------|-------------------|---------------------------------------------------| 16 | |PRIORITY |30 | | 17 | |MAX |10 |number of backups kept before deleting the oldest | 18 | |BAKROOT |[next to devroot] | | 19 | -------------------------------------------------------------------------------- /docs/plugins/minify.md: -------------------------------------------------------------------------------- 1 | # Minify 2 | 3 | Type: `content` 4 | Enabled by default: `true` 5 | 6 | - compress JS files (js) 7 | - compile and compress SASS/CSS (sass, scss) 8 | 9 | 10 | --- 11 | **config.yaml key:** MINIFY 12 | |argument |default value |explanation | 13 | |-----------------------|-------------------|-----------------------------------------------| 14 | |PRIORITY |3 | | 15 | |JS_OBFUSCATE |true | | 16 | |JS_OBFUSCATE_GLOBALS |true | | 17 | |CSS_INCLUDE_COMMENTS |false | | 18 | |CSS_OUTPUT_STYLE |compressed | | 19 | -------------------------------------------------------------------------------- /docs/plugins/pixelizer.md: -------------------------------------------------------------------------------- 1 | # Pixelizer 2 | 3 | Type: `content` 4 | Enabled by default: `false` 5 | 6 | - compress and resize images (png, jpg, jpeg, tif, tiff, bmp) 7 | - also convert these images to webp 8 | - turn `` tags in markdown files into `` tags in HTML, offering webp (and the original format as a fallback) to the browser 9 | - targets for resizing/compression, as well as the layouts of the `` tag, are configurable freely 10 | 11 | --- 12 | 13 | With the standard config, this: 14 | 15 | ```md 16 | ![Test Image](some/source.jpg) 17 | ``` 18 | 19 | will be turned into this: 20 | 21 | ```HTML 22 | 23 | 24 | 25 | Test Image 26 | 27 | ``` 28 | 29 | instead of this: 30 | 31 | ```HTML 32 | Test Image 33 | ``` 34 | 35 | Additionally, all 6 variants will be created (resized, quality changed, format changed), and the original will be copied as fallback. 36 | 37 | --- 38 | **config.yaml key:** PIXELIZER 39 | |argument |default value |explanation | 40 | |--------------|-------------------|--------------------------------------------| 41 | |PRIORITY |3 | | 42 | |TARGETS |- lg 1000 70
    - md 650 70
    - sm 300 70|syntax: \ \ \| 43 | |LAYOUTS |- (max-width: 1000px) 100vw
    - 1000px |these are essentially media queries. Wrap in `"..."` in yaml! | 44 | -------------------------------------------------------------------------------- /docs/plugins/readingtime.md: -------------------------------------------------------------------------------- 1 | # Reading Time 2 | 3 | Type: `content` 4 | Enabled by default: `true` 5 | 6 | Estimates the reading time in minutes for a page or post. Common feature for blogs. 7 | 8 | If WPM_FAST does not equal WPM_SLOW, displays an estimate of `x - y` minutes. Otherwise, only a single number is displayed. 9 | 10 | --- 11 | 12 | Display the reading time like this in your template: 13 | ```html 14 | {{ reading_time }} 15 | ``` 16 | 17 | --- 18 | **config.yaml key:** READING_TIME 19 | |argument |default value |explanation | 20 | |-----------------------|-------------------|-----------------------------------------------| 21 | |PRIORITY |850 | | 22 | |WPM_FAST |265 |for fast readers | 23 | |WPM_SLOW |90 |for slow readers | 24 | |SEPARATOR | - |separator between the fast and slow values | 25 | -------------------------------------------------------------------------------- /docs/plugins/sftp.md: -------------------------------------------------------------------------------- 1 | # SFTP 2 | 3 | Type: `publication` 4 | Enabled by default: `false` 5 | 6 | Uploads your webroot to an SFTP Webserver after you have exited barely. 7 | 8 | Works with either a username/password, or an SSH key. 9 | 10 | --- 11 | **config.yaml key:** SFTP 12 | |argument |default value |explanation | 13 | |-----------------------|-------------------|-----------------------------------------------------------------------------------| 14 | |PRIORITY |90 | | 15 | |HOSTNAME | | | 16 | |username | | | 17 | |PASSWORD | | | 18 | |KEY | |if a key is specified, it will be preferred over a password | 19 | |ROOT | |dir for the files on the server. check with your host for a correct absolute path! | 20 | -------------------------------------------------------------------------------- /docs/plugins/timestamps.md: -------------------------------------------------------------------------------- 1 | # Timestamps 2 | 3 | Type: `content` 4 | Enabled by default: `false` 5 | 6 | Timestamp your pages and posts. Generates stamps for the creation and last modified times. Displays them in a custom time format. 7 | 8 | **Note**: On Unix systems, there is no "creation" timestamp. It always gets reset to the last modified time. 9 | 10 | --- 11 | 12 | Display the timestamps like this in your template: 13 | ```html 14 | {{ created }} 15 | {{ edited }} 16 | ``` 17 | 18 | --- 19 | **config.yaml key:** TIMESTAMPS 20 | |argument |default value |explanation | 21 | |-----------------------|-------------------|-----------------------------------------------| 22 | |PRIORITY |3 | | 23 | |FORMAT |%d.%m.%Y | | 24 | -------------------------------------------------------------------------------- /docs/plugins/toc.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | Type: `content` 4 | Enabled by default: `true` 5 | 6 | Creates a nicely structured table of contents by assigning IDs to your headings and linking to them. You can specify the min and max levels of headings, and whether it should be an unordered list, an ordered list, or some other wrapper tag should be used. 7 | 8 | --- 9 | 10 | Display the table of contents like this in your template: 11 | ```html 12 | {{ toc }} 13 | ``` 14 | 15 | **Note:** unfortunately, the ToC does not support modular subpages, as their positioning and order heavily depensd on the jinja2 template used. ToCs can still be generated for the parent page, however. 16 | 17 | --- 18 | **config.yaml key:** TOC 19 | |argument |default value |explanation | 20 | |-----------------------|-------------------|-----------------------------------------------| 21 | |PRIORITY |2 | | 22 | |MIN_DEPTH |1 | | 23 | |MAX_DEPTH |4 | | 24 | |LIST_ELEMENT |ul |ol for ordered list | 25 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1718714799, 6 | "narHash": "sha256-FUZpz9rg3gL8NVPKbqU8ei1VkPLsTIfAJ2fdAf5qjak=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "c00d587b1a1afbf200b1d8f0b0e4ba9deb1c7f0e", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "utils": "utils" 23 | } 24 | }, 25 | "systems": { 26 | "locked": { 27 | "lastModified": 1681028828, 28 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 29 | "owner": "nix-systems", 30 | "repo": "default", 31 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "nix-systems", 36 | "repo": "default", 37 | "type": "github" 38 | } 39 | }, 40 | "utils": { 41 | "inputs": { 42 | "systems": "systems" 43 | }, 44 | "locked": { 45 | "lastModified": 1710146030, 46 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 47 | "owner": "numtide", 48 | "repo": "flake-utils", 49 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "numtide", 54 | "repo": "flake-utils", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | utils.url = "github:numtide/flake-utils"; 4 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 5 | }; 6 | 7 | outputs = { self, nixpkgs, utils }: 8 | utils.lib.eachDefaultSystem (system: 9 | let 10 | pkgs = import nixpkgs { inherit system; }; 11 | in 12 | { 13 | packages.default = pkgs.python312Packages.buildPythonPackage rec { 14 | pname = "barely"; 15 | version = "1.2.2"; 16 | 17 | src = pkgs.fetchPypi { 18 | inherit pname version; 19 | hash = "sha256-/gliqfkPwnhZilFXltNXuOjQnJoJ9u0SnktqlLmRfTo="; 20 | }; 21 | 22 | 23 | doCheck = false; 24 | 25 | pyproject = true; 26 | build-system = [ pkgs.python312Packages.setuptools ]; 27 | 28 | propagatedBuildInputs = with pkgs.python312Packages; [ 29 | pip 30 | click 31 | click-default-group 32 | coloredlogs 33 | mock 34 | pyyaml 35 | watchdog 36 | pillow 37 | gitpython 38 | pygments 39 | libsass 40 | pysftp 41 | livereload 42 | binaryornot 43 | jinja2 44 | mistune 45 | calmjs 46 | ] ++ [ pkgs.python312Full ]; 47 | }; 48 | 49 | devShells.default = pkgs.mkShell { 50 | packages = with pkgs; [ 51 | python39Full 52 | python39Packages.pip 53 | python39Packages.platformdirs 54 | 55 | ruff 56 | djlint 57 | ]; 58 | 59 | LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc.lib pkgs.file ]; 60 | 61 | shellHook = /* bash */ '' 62 | set_if_unset() { 63 | if [ -z "$(eval \$$1)" ]; then 64 | export "$1"="$2" 65 | fi 66 | } 67 | 68 | # Setting LD_LIBRARY_PATH can cause issues on non-NixOS systems 69 | if ! command -v nixos-version &> /dev/null; then 70 | unset LD_LIBRARY_PATH 71 | fi 72 | 73 | SOURCE_DATE_EPOCH=$(date +%s) 74 | VENV=.venv 75 | if [ -d $VENV ]; then 76 | source ./$VENV/bin/activate 77 | fi 78 | export PYTHONPATH=`pwd`/$VENV/${pkgs.python39Full.sitePackages}/:$PYTHONPATH 79 | ''; 80 | }; 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [flake8] 5 | ignore = E501 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import setuptools 4 | 5 | # The directory containing this file 6 | HERE = pathlib.Path(__file__).parent 7 | 8 | # The text of the README file 9 | README = (HERE / "README.md").read_text() 10 | 11 | setuptools.setup( 12 | name="barely", 13 | version="1.2.2", 14 | description="barely is a lightweight, but highly extensible static site generator written in pure python.", 15 | long_description=README, 16 | long_description_content_type="text/markdown", 17 | keywords=["static site generator", "jinja2", "markdown", "web development", "blog"], 18 | url="https://notablog.io", 19 | download_url="https://github.com/charludo/barely/archive/refs/tags/v1.2.2.tar.gz", 20 | author="Charlotte Hartmann Paludo", 21 | author_email="barely@charlotteharludo.com", 22 | license="GPL-3.0", 23 | packages=setuptools.find_packages(), 24 | include_package_data=True, 25 | zip_safe=False, 26 | entry_points={"console_scripts": ["barely = barely.cli:run"]}, 27 | python_requires=">=3.9.0", 28 | install_requires=[ 29 | "click>=8.0.0", 30 | "click-default-group>=1.2.2", 31 | "coloredlogs>=15.0.0", 32 | "mock>=4.0.0", 33 | "pyyaml>=5.3.0", 34 | "watchdog>=2.0.0", 35 | "pillow>=8.0.0", 36 | "GitPython>=3.0.0", 37 | "pygments>=2.5.0", 38 | "libsass>=0.21.0", 39 | "pysftp>=0.2.5", 40 | "livereload>=2.5.0", 41 | "binaryornot>=0.4.0", 42 | "jinja2>=3.0.0", 43 | "mistune>=2.0.0rc1", 44 | "calmjs>=3.3.0", 45 | ], 46 | classifiers=[ 47 | "Development Status :: 5 - Production/Stable", 48 | "Intended Audience :: Developers", 49 | "Topic :: Text Processing :: Markup :: HTML", 50 | "Topic :: Text Processing :: General", 51 | "Topic :: Software Development :: Build Tools", 52 | "Topic :: Software Development", 53 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 54 | "Natural Language :: English", 55 | "Programming Language :: Python :: 3.9", 56 | "Environment :: Console", 57 | "Operating System :: OS Independent", 58 | ], 59 | ) 60 | --------------------------------------------------------------------------------