├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── container-image.yaml │ └── tests.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── deploy ├── README.md ├── caddy-authelia │ ├── .env-example │ ├── Caddyfile │ ├── README.md │ ├── configuration-example.yml │ ├── containers.svg │ └── docker-compose.yaml ├── caddy-basic-auth │ ├── .env-example │ ├── Caddyfile │ ├── README.md │ └── docker-compose.yaml └── docker-local │ └── docker-compose.yaml ├── entrypoint.sh ├── pyproject.toml ├── scripts └── dump.sh ├── silicon ├── __init__.py ├── commands.py ├── db.py ├── docs │ ├── main.md │ └── syntax.md ├── j2_filters.py ├── page.py ├── related.py ├── render_md.py ├── schema.sql ├── static │ ├── css │ │ ├── colors-light.css │ │ ├── edit.css │ │ ├── history.css │ │ ├── layout.css │ │ ├── main.css │ │ ├── print.css │ │ ├── pygments.css │ │ └── search.css │ ├── img │ │ ├── favicon-edit-16.png │ │ ├── favicon-edit-192.png │ │ ├── favicon-edit-32.png │ │ ├── favicon-edit.ico │ │ ├── favicon-view-16.png │ │ ├── favicon-view-192.png │ │ ├── favicon-view-32.png │ │ ├── favicon-view.ico │ │ └── silicon-logo.png │ ├── js │ │ ├── edit.js │ │ ├── main.js │ │ └── widgets.js │ ├── package-lock.json │ └── package.json ├── templates │ ├── base.html.j2 │ ├── docs.html.j2 │ ├── edit.html.j2 │ ├── history.html.j2 │ ├── macros.html.j2 │ ├── not_found.html.j2 │ ├── related.html.j2 │ ├── search.html.j2 │ └── view.html.j2 ├── util.py └── views.py ├── tests ├── __init__.py ├── conftest.py ├── test_pages.py ├── test_related.py ├── test_toc.py ├── toc_in.md └── toc_out.html └── uv.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | instance/ 2 | **/node_modules/ 3 | Dockerfile* 4 | *.md 5 | deploy/ 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*.{js,py,md,css,j2,html,yaml,sh,toml}] 10 | charset = utf-8 11 | 12 | [*.{html,yaml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.github/workflows/container-image.yaml: -------------------------------------------------------------------------------- 1 | name: Build and push tagged container image 2 | 3 | on: 4 | push: 5 | tags: ['*'] 6 | 7 | env: 8 | REPO: silicon 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: docker/setup-qemu-action@v3 16 | - uses: docker/setup-buildx-action@v3 17 | - uses: docker/login-action@v3 18 | with: 19 | username: ${{ vars.DOCKER_NAMESPACE }} 20 | password: ${{ secrets.DOCKER_TOKEN }} 21 | - uses: docker/build-push-action@v5 22 | with: 23 | push: true 24 | platforms: linux/amd64,linux/arm64 25 | tags: | 26 | ${{ vars.DOCKER_REGISTRY }}/${{ vars.DOCKER_NAMESPACE}}/${{ env.REPO }}:latest 27 | ${{ vars.DOCKER_REGISTRY }}/${{ vars.DOCKER_NAMESPACE}}/${{ env.REPO }}:${{ github.ref_name }} 28 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Integration tests 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - docker-compose.yaml 7 | - .editorconfig 8 | - '*.md' 9 | - 'deploy/**' 10 | workflow_dispatch: 11 | pull_request: 12 | 13 | jobs: 14 | integration-tests: 15 | strategy: 16 | matrix: 17 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install poetry deps and run tests 28 | run: | 29 | pipx install uv 30 | uv run flake8 --show-source --exclude .venv 31 | uv run pytest 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .venv 3 | instance/ 4 | silicon/static/node_modules 5 | __pycache__ 6 | *.egg-info/ 7 | .pytest_cache/ 8 | NOTES.md 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Prettier does "opinionated" things to unordered lists in Markdown: 2 | # https://github.com/prettier/prettier/issues/5019 3 | # https://github.com/prettier/prettier/issues/5308 4 | **/*.md 5 | **/*.yaml 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.formatOnSave": true 4 | }, 5 | "[css]": { 6 | "editor.formatOnSave": true 7 | }, 8 | "[markdown]": { 9 | "editor.formatOnSave": false 10 | }, 11 | "[python]": { 12 | "editor.formatOnSave": false 13 | }, 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_IMAGE=docker.io/library/python:3.13-slim 2 | ARG NODE_IMAGE=docker.io/library/node:lts-alpine 3 | 4 | ### Stage 1: Install JS dependencies 5 | FROM $NODE_IMAGE AS npm-install 6 | 7 | COPY ./ /staging/ 8 | WORKDIR /staging/silicon/static 9 | RUN npm ci 10 | 11 | ### Stage 2: Install Silicon and dependencies 12 | FROM $PYTHON_IMAGE AS build 13 | 14 | # uv settings 15 | ARG UV_LINK_MODE=copy 16 | ARG UV_COMPILE_BYTECODE=1 17 | ARG UV_PYTHON_DOWNLOADS=never 18 | ARG UV_LOCKED=1 19 | ARG UV_PROJECT_ENVIRONMENT=/silicon/.venv 20 | 21 | COPY --from=npm-install /staging /staging 22 | WORKDIR /staging 23 | 24 | RUN --mount=type=cache,target=/root/.cache < 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | # Tech Stack 38 | 39 | Projects we rely on and appreciate! 40 | 41 | * [Python](https://www.python.org/), of course. 42 | * [uv](https://github.com/astral-sh/uv) for project management. 43 | * [Flask](https://flask.palletsprojects.com/), the micro-framework. 44 | * [Mistune](https://github.com/lepture/mistune) to render Markdown into HTML. 45 | * [Pygments](https://pygments.org/) for syntax highlighting of code blocks. 46 | * [python-slugify](https://github.com/un33k/python-slugify) creates URL-friendly 47 | "slugs" from strings. 48 | * [python-dotenv](https://github.com/theskumar/python-dotenv) for configuration 49 | management. 50 | * [Gunicorn](https://gunicorn.org/) for deployment. 51 | * [Pytest](https://pytest.org/) and 52 | [Beautiful Soup](https://www.crummy.com/software/BeautifulSoup/) for functional testing. 53 | * [CodeMirror](https://codemirror.net/) (optional) for editor syntax 54 | highlighting 55 | 56 | # Quickstart 57 | 58 | ## Running Locally 59 | 60 | See the Development section below for steps on running the app locally. 61 | 62 | For production, this project is configured and deployed much like any other 63 | Flask project. For details, see the [Flask configuration handling Docs]. 64 | 65 | [Flask configuration handling docs]: https://flask.palletsprojects.com/en/stable/config/ 66 | 67 | ## Docker or Podman 68 | 69 | If containers are more your speed, the following commands should get you going. 70 | You obviously need to have [Docker](https://www.docker.com) installed. (If you 71 | have [Podman](https://podman.io), simply substitute `docker` for `podman`.) 72 | 73 | ```sh 74 | docker run \ 75 | -ti \ 76 | --rm \ 77 | -p 127.0.0.1:5000:5000 \ 78 | -v silicon_instance:/home/silicon/instance \ 79 | docker.io/bityard/silicon 80 | ``` 81 | 82 | And then open http://localhost:5000/ with your local web browser. 83 | 84 | You could also start it with `docker-compose` (or `docker compose`): 85 | 86 | ```sh 87 | cd deploy/docker-local 88 | docker-compose up 89 | ``` 90 | 91 | If you want to build the image, a `Dockerfile` and a `docker-compose.yaml` file 92 | are provided, so you can build the container with: 93 | 94 | ```sh 95 | docker build -t docker.io/bityard/silicon . 96 | ``` 97 | 98 | Or with buildah: 99 | 100 | ```sh 101 | buildah build --format docker -t docker.io/bityard/silicon . 102 | ``` 103 | 104 | Silicon will listen on port 5000 (plaintext HTTP) and stores all application 105 | data in `/home/silicon/instance`. 106 | 107 | # Development 108 | 109 | ## Prerequisites 110 | 111 | This repo tries to be somewhat flexible about its tools and workflow. However, 112 | along the happy path you will find some combination of Python 3.9 (or better), 113 | `uv`, Docker/Podman, and `npm`. 114 | 115 | Install [uv](https://github.com/astral-sh/uv) if necessary. If you are not a fan 116 | of curlpipes, there are many other ways to install it: 117 | 118 | * `pip install --user uv` 119 | * `pipx install uv` 120 | * download binaries from the [latest release](https://github.com/astral-sh/uv) 121 | 122 | ## Setup 123 | 124 | Some settings can either be set as environment variables or written to a 125 | file named `.env` in the project root. For development, this will suffice: 126 | 127 | ```sh 128 | WERKZEUG_DEBUG_PIN=off 129 | ``` 130 | 131 | You can set any environment variables mentioned in the Flask or Werkzeug 132 | docs, but these are some you might care to know about: 133 | 134 | * `FLASK_RUN_HOST`: defaults to `127.0.0.1` 135 | * `FLASK_RUN_PORT`: defaults to `5000` 136 | * `INSTANCE_PATH`: where the silicon data (in particular the database) is stored 137 | * `WERKZEUG_DEBUG_PIN`: the PIN to enable the Werkzeug debug console. Set to 138 | "off" to disable it if you are sure the app is only listening on localhost. 139 | * `SECRET_KEY`: A string used in session cookies. For development purposes, this 140 | can be anything, but for production it should be a 16-byte (or larger) string 141 | of random characters. Setting this is optional as the app will create one 142 | (and write it to a file in `INSTANCE_PATH`) if one doesn't exist. 143 | * `SILICON_EDITOR`: When set to `textarea`, this disables the CodeMirror text 144 | editor when editing pages and uses a standard textarea element instead. 145 | 146 | To initialize the database after the configuration settings have been set, 147 | run the following command. It will create an `instance` directory in the root 148 | of the project and initialize the SQLite database from `schema.sql`. 149 | 150 | ```sh 151 | uv run flask --app silicon init-db 152 | ``` 153 | 154 | ## Running Silicon 155 | 156 | Run the project via the `flask` development server: 157 | 158 | ```sh 159 | uv run flask --app silicon run --debug 160 | ``` 161 | 162 | Unless you changed the defaults, you should be able to access the UI on 163 | http://localhost:5000/ 164 | 165 | ## Running tests and flake8 166 | 167 | To run the tests: 168 | 169 | ```sh 170 | uv run pytest 171 | ``` 172 | 173 | If you have a tmpfs filesystem, you can set the `TMP` environment variable to 174 | have test databases created there (which is faster and results in less 175 | wear-and-tear on your disk): 176 | 177 | ```sh 178 | TMP=/dev/shm uv run pytest 179 | ``` 180 | 181 | To make sure all code is formatted to flake8 standards, run `flake8`: 182 | 183 | ```sh 184 | uv run flake8 --exclude .venv 185 | ``` 186 | 187 | # Production Deployment 188 | 189 | Silicon Notes is a fairly simple web application which contains no built-in 190 | authentication or authorization mechanisms whatsoever. If deploying the 191 | application on its own, you should only deploy this to a trusted private network 192 | such as a local LAN segregated from the public Internet by a firewall or VPN. 193 | **If deploying on a public server, you are responsible for ensuring all access 194 | to it is secure.** 195 | 196 | The `deploy` direcctory contains various sample deployments that may be helpful 197 | as starting points for a production deployment. 198 | 199 | Normally, it is easiest to host applications like this on their own domain or 200 | subdomain, such as https://silicon.example.com/. If you would rather host it 201 | under a prefix instead (as in https://example.com/silicon), see [this 202 | issue](https://github.com/cu/silicon/issues/3) for hints on how to do that. 203 | 204 | # Configuring the CodeMirror Editor 205 | 206 | Support for [CodeMirror](https://codemirror.net) as a text editor is included by 207 | default. It does add a lot of "heft" to the UI, mostly around having to make a 208 | separate network request for each language and addon specified. To use it, you 209 | also have to install third-party Javascript/CSS static packages by running ONE 210 | of the following commands: 211 | 212 | ```sh 213 | # If you have `npm` installed locally 214 | (cd silicon/static && npm ci) 215 | ``` 216 | 217 | Or: 218 | 219 | ```sh 220 | # if you have `docker` installed 221 | docker run -ti --rm -v $PWD/silicon/static:/app -w /app node:alpine npm ci 222 | ``` 223 | 224 | Currently only a handful of languages are enabled for syntax highlighting, if 225 | you want to edit the list to suit your needs, you can edit 226 | `silicon/static/js/edit.js`. You can find a list of supported lanauges 227 | [here](https://codemirror.net/mode/). 228 | 229 | To disable CodeMirror and use a regular textarea instead, add the following to 230 | your `.env` file or environment: 231 | 232 | ```sh 233 | SILICON_EDITOR=textarea 234 | ``` 235 | 236 | # Data Export and Import 237 | 238 | ## SQL 239 | 240 | In the event that a database migration is needed, follow these steps: 241 | 242 | 1. Stop the Silicon instance. 243 | 2. Pull down the latest version of this repository. 244 | 3. Run `scripts/dump.sh > silicon_data.sql`. 245 | 246 | To import the data: 247 | 248 | 4. Move or rename the old `instance/silicon.sqlite`, if it exists. 249 | 5. Run `uv run flask --app silicon init-db`. 250 | 6. Run `sqlite3 instance/silicon.sqlite < silicon_data.sql`. 251 | 7. Start the Silicon instance. 252 | 253 | Once you are satisfied that there are no issues, you can archive (or delete) 254 | the old `silicon.sqlite` file and `silicon_data.sql`. 255 | 256 | ## JSON 257 | 258 | If you want to dump your data as JSON (perhaps to import into another system 259 | or hack on with your own tools), these scripts are not well tested but might do 260 | the job. 261 | 262 | Export: 263 | 264 | ```sh 265 | #!/usr/bin/env sh 266 | 267 | DB=instance/silicon.sqlite 268 | 269 | for table in pages relationships; do 270 | sqlite3 $DB -cmd '.mode json' "select * from $table;" > $table.json 271 | done 272 | ``` 273 | 274 | Import: 275 | 276 | ```sh 277 | #!/usr/bin/env sh 278 | 279 | sqlite3 instance/silicon.sqlite << EOF 280 | INSERT INTO pages (revision, title, body) 281 | SELECT 282 | json_extract(value, '$.revision'), 283 | json_extract(value, '$.title'), 284 | json_extract(value, '$.body') 285 | FROM json_each(readfile('pages.json')); 286 | 287 | INSERT INTO relationships 288 | SELECT 289 | json_extract(value, '$.title_a'), 290 | json_extract(value, '$.title_a') 291 | FROM json_each(readfile('relationships.json')); 292 | EOF 293 | ``` 294 | 295 | # Suggested Contributions 296 | 297 | ## Clean Up CSS 298 | 299 | The current style sheets were more or less arrived at by trial and error. Any 300 | help in organizing the rules in a more coherent yet extensible way would be 301 | much appreciated. 302 | 303 | ## Dark Theme 304 | 305 | It would be nice if the CSS adjusted itself to a dark theme based on the 306 | preference set by the user in the browser. This should be pretty easy since 307 | almost all of the colors are in one file. 308 | 309 | ## Clean up Javascript 310 | 311 | To put it mildly, my JS skills are not the best. I would very much appreciate 312 | any suggestions on improvements to the small amount of code there is, or 313 | whether there is a better way to organize it. I won't bring in any kind of 314 | Javascript "build" tool as a project dependency, though. 315 | 316 | ## Diffs Between Revisions 317 | 318 | This is pretty standard on wiki-like apps, but it's not a critical feature 319 | for me so I haven't yet mustered up the fortitude to implement it. (It would 320 | also likely involve adding a diff library as a dependency.) 321 | 322 | ## Draft Feature / Autosave / Leap-frog Detection 323 | 324 | To prevent the loss of unsaved changes while editing, we use the browser's 325 | "are you sure?" nag if there has been a change to the editing area since the 326 | page was loaded. However, there are still (at least) two opportunies to lose 327 | work: 328 | 329 | 1. The browser crashes. 330 | 2. Two simulatenous edits of a page in separate tabs or windows. 331 | 332 | The first is rare and the second is not as serious since both revisions are 333 | saved. But it is currently up to the user to recognize what happened and 334 | remedy the situation by hand. 335 | 336 | These are technically three separate features but I believe they would be 337 | quite closely coupled if implemented together. 338 | 339 | ## Refine Tests 340 | 341 | The `tests` directory contains functional tests that were deemed the most 342 | important. But they could be better organized and optimized/flexible. Code 343 | coverage is not likely very high. Some tests are lacking or missing because I 344 | was not able to work out the right way to test certain things. 345 | 346 | ## Add Anchors to Headings 347 | 348 | Anchors on headers are a very common feature on various CMSes. They let you 349 | link directly to headings, e.g.: 350 | 351 | http://example.com/view/page_title#some-section 352 | 353 | ## Implement a (Better) Task List Plugin 354 | 355 | Mistune (the Markdown->HTML renderer) ships with a [task_lists plugin]. It is 356 | functional, but it renders task list items inside an ordinary `