├── .github └── workflows │ ├── release-docker.yml │ └── release.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE.md ├── Makefile ├── README.md ├── cli ├── .gitignore ├── Dockerfile ├── builder │ ├── builder.go │ ├── obsidian.go │ └── obsidian_test.go ├── config.yml.example ├── config │ └── config.go ├── go.mod ├── go.sum └── main.go ├── common ├── config.go ├── db.go ├── go.mod ├── go.sum ├── id.go ├── id_test.go ├── note.go └── urils.go ├── go.work ├── go.work.sum ├── package.json ├── pnpm-lock.yaml ├── screenshots └── markdownbrain.png ├── server.config.example ├── server ├── .dockerignore ├── .env ├── .gitignore ├── Dockerfile ├── config │ └── config.go ├── go.mod ├── go.sum ├── main.go ├── templates │ ├── 404.html │ ├── 500.html │ ├── home.html │ ├── js.html │ ├── note.html │ └── welcome.html └── www │ ├── config.yml │ ├── data │ └── .keep │ └── static │ ├── app.css │ ├── app.js │ ├── favicon.ico │ └── htmx.min.js ├── tailwind.config.js └── tailwind.css /.github/workflows/release-docker.yml: -------------------------------------------------------------------------------- 1 | name: Release Docker 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | server: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v3 17 | 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v3 20 | 21 | - name: Docker meta 22 | id: meta-server 23 | uses: docker/metadata-action@v5 24 | with: 25 | images: ghcr.io/${{ github.repository }}-server 26 | tags: | 27 | type=semver,pattern={{version}} 28 | type=semver,pattern={{major}}.{{minor}} 29 | 30 | - name: Login to GitHub Container Registry 31 | uses: docker/login-action@v3 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GH_TOKEN }} 36 | 37 | - name: Build and push server 38 | uses: docker/build-push-action@v5 39 | with: 40 | context: . 41 | file: ./server/Dockerfile 42 | platforms: linux/amd64,linux/arm64 43 | push: true 44 | tags: ${{ steps.meta-server.outputs.tags }} 45 | labels: ${{ steps.meta-server.outputs.labels }} 46 | 47 | cli: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | 53 | - name: Set up QEMU 54 | uses: docker/setup-qemu-action@v3 55 | 56 | - name: Set up Docker Buildx 57 | uses: docker/setup-buildx-action@v3 58 | 59 | - name: Docker meta 60 | id: meta-cli 61 | uses: docker/metadata-action@v5 62 | with: 63 | images: ghcr.io/${{ github.repository }}-cli 64 | tags: | 65 | type=semver,pattern={{version}} 66 | type=semver,pattern={{major}}.{{minor}} 67 | 68 | - name: Login to GitHub Container Registry 69 | uses: docker/login-action@v3 70 | with: 71 | registry: ghcr.io 72 | username: ${{ github.actor }} 73 | password: ${{ secrets.GH_TOKEN }} 74 | 75 | - name: Build and push cli 76 | uses: docker/build-push-action@v5 77 | with: 78 | context: . 79 | file: ./cli/Dockerfile 80 | platforms: linux/amd64,linux/arm64 81 | push: true 82 | tags: ${{ steps.meta-cli.outputs.tags }} 83 | labels: ${{ steps.meta-cli.outputs.labels }} 84 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Binaries 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: '1.23' 19 | 20 | - name: Build All Binaries 21 | run: | 22 | mkdir -p dist 23 | # Server builds 24 | GOOS=linux GOARCH=amd64 go build -o dist/markdownbrain-server-linux-amd64 server/main.go 25 | GOOS=linux GOARCH=arm64 go build -o dist/markdownbrain-server-linux-arm64 server/main.go 26 | GOOS=windows GOARCH=amd64 go build -o dist/markdownbrain-server-windows-amd64.exe server/main.go 27 | GOOS=darwin GOARCH=amd64 go build -o dist/markdownbrain-server-darwin-amd64 server/main.go 28 | GOOS=darwin GOARCH=arm64 go build -o dist/markdownbrain-server-darwin-arm64 server/main.go 29 | 30 | # CLI builds 31 | GOOS=linux GOARCH=amd64 go build -o dist/markdownbrain-cli-linux-amd64 cli/main.go 32 | GOOS=linux GOARCH=arm64 go build -o dist/markdownbrain-cli-linux-arm64 cli/main.go 33 | GOOS=windows GOARCH=amd64 go build -o dist/markdownbrain-cli-windows-amd64.exe cli/main.go 34 | GOOS=darwin GOARCH=amd64 go build -o dist/markdownbrain-cli-darwin-amd64 cli/main.go 35 | GOOS=darwin GOARCH=arm64 go build -o dist/markdownbrain-cli-darwin-arm64 cli/main.go 36 | 37 | - name: Create Release 38 | uses: softprops/action-gh-release@v1 39 | with: 40 | files: dist/* 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | node_modules/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Server", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/server", 13 | "env": { 14 | "DEV_MODE": "true" 15 | }, 16 | "cwd": "${workspaceFolder}/server" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "markdownbrain" 4 | ] 5 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build build-server build-cli 2 | 3 | build: build-server build-cli 4 | 5 | build-server: 6 | go build -o bin/markdownbrain server/main.go 7 | 8 | build-cli: 9 | go build -o bin/markdownbrain-cli cli/main.go 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

MarkdownBrain

2 |

3 | 4 | 5 | 6 | 7 | markdownbrain 8 | 9 | 10 |
11 |

12 |

Make your digital garden with MarkdownBrain.

13 |

14 | 15 | MarkdownBrain Build 16 | 17 | 18 | MarkdownBrain Build 19 | 20 | 21 | License 22 | 23 |

24 |

25 | Releases · 26 | Documentation 27 |

28 | 29 | ## Quick Start 30 | 31 | ### Client 32 | 33 | #### Create client config.yml 34 | 35 | ```bash 36 | echo 'source: "~/Library/Mobile Documents/com~apple~CloudDocs/obsidian/example" 37 | server: "https://your-server-url" 38 | api_key: "1234567890" 39 | ignores: 40 | - "Templates"' > config.yml 41 | ``` 42 | 43 | > Note: The `source` is the path to your Obsidian vault. 44 | 45 | #### Run cli 46 | 47 | ```bash 48 | curl -L https://github.com/blackstorm/markdownbrain/releases/download/v0.1.1/markdownbrain-cli-darwin-amd64 -o markdownbrain-client 49 | chmod +x markdownbrain-client 50 | ./markdownbrain-client -c config.yml 51 | ``` 52 | > Note: Before running the client, ensure the `server` is running. 53 | 54 | ### Server 55 | 56 | #### Create config.yml 57 | ```bash 58 | echo 'lang: "en" 59 | root_note_name: "Welcome" 60 | name: "MarkdownBrain" 61 | description: "MarkdownBrain" 62 | api_key: "1234567890"' > config.yml 63 | ``` 64 | 65 | #### Run Server 66 | 67 | ```bash 68 | docker run -dit --name markdownbrain -v $(pwd)/config.yml:/markdownbrain/config.yml -p 3000:3000 ghcr.io/blackstorm/markdownbrain-server:latest 69 | ``` 70 | 71 | ## Documentation 72 | 73 | [MarkdownBrain.com](https://markdownbrain.com) 74 | 75 | ## License 76 | 77 | [AGPLv3](LICENSE.md) 78 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | config.yaml 2 | config.yml -------------------------------------------------------------------------------- /cli/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-bookworm AS builder 2 | 3 | WORKDIR /markdownbrain 4 | 5 | COPY . . 6 | 7 | RUN go build -o markdownbrain-cli cli/main.go 8 | 9 | FROM debian:bookworm 10 | 11 | WORKDIR /markdownbrain 12 | 13 | COPY --from=builder /markdownbrain/cli/config.yml.example /markdownbrain/config.yml 14 | 15 | CMD ["./markdownbrain-cli", "-config", "/markdownbrain/config.yml"] 16 | -------------------------------------------------------------------------------- /cli/builder/builder.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "github.com/blackstorm/markdownbrain/common" 5 | ) 6 | 7 | type DatabaseBuilder interface { 8 | Build(source string, db *common.DB) error 9 | } 10 | -------------------------------------------------------------------------------- /cli/builder/obsidian.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/blackstorm/markdownbrain/common" 14 | strip "github.com/grokify/html-strip-tags-go" 15 | "github.com/yuin/goldmark" 16 | "github.com/yuin/goldmark/extension" 17 | "github.com/yuin/goldmark/renderer/html" 18 | "gopkg.in/yaml.v3" 19 | ) 20 | 21 | type Error struct { 22 | Message string 23 | Err error 24 | } 25 | 26 | func (e *Error) Error() string { 27 | return fmt.Sprintf("%s: %v", e.Message, e.Err) 28 | } 29 | 30 | type NoteData struct { 31 | ID string 32 | Title string 33 | Description string 34 | FilePath string 35 | CreatedAt string 36 | LastUpdatedAt string 37 | } 38 | 39 | type ObsidianBuilder struct { 40 | idGenerator *common.IdGenerator 41 | ignores map[string]bool 42 | db *common.DB 43 | md goldmark.Markdown 44 | } 45 | 46 | func NewObsidianBuilder(ignores []string, db *common.DB) *ObsidianBuilder { 47 | ignoremap := make(map[string]bool) 48 | for _, i := range ignores { 49 | ignoremap[i] = true 50 | } 51 | 52 | md := goldmark.New( 53 | goldmark.WithExtensions(extension.GFM), 54 | goldmark.WithRendererOptions( 55 | html.WithUnsafe(), 56 | html.WithHardWraps(), 57 | ), 58 | ) 59 | 60 | return &ObsidianBuilder{ 61 | idGenerator: common.NewSqidsIdGenerator(), 62 | ignores: ignoremap, 63 | db: db, 64 | md: md, 65 | } 66 | } 67 | 68 | // Build build from a note source dir 69 | func (b *ObsidianBuilder) Build(src string) error { 70 | srcPath := b.resolveHomePath(src) 71 | 72 | info, err := os.Stat(srcPath) 73 | if err != nil { 74 | return &Error{"Failed to access source path", err} 75 | } 76 | 77 | if !info.IsDir() { 78 | return &Error{"Source path is not a directory", nil} 79 | } 80 | 81 | notes, err := b.collectNotes(srcPath) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | return b.processNotes(notes) 87 | } 88 | 89 | // collectNotes collect src path all 90 | func (b *ObsidianBuilder) collectNotes(srcPath string) (map[string]*NoteData, error) { 91 | notes := make(map[string]*NoteData) 92 | 93 | err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error { 94 | if err != nil { 95 | return err 96 | } 97 | 98 | for component := range b.ignores { 99 | if strings.Contains(path, component) { 100 | return nil 101 | } 102 | } 103 | 104 | if !info.IsDir() && strings.HasSuffix(strings.ToLower(path), ".md") { 105 | note, err := b.getNoteMetadata(path) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | filename := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) 111 | notes[filename] = note 112 | } 113 | 114 | return nil 115 | }) 116 | 117 | if err != nil { 118 | return nil, &Error{"Failed to collect notes", err} 119 | } 120 | 121 | return notes, nil 122 | } 123 | 124 | func (b *ObsidianBuilder) processNotes(notes map[string]*NoteData) error { 125 | for _, data := range notes { 126 | content, err := os.ReadFile(data.FilePath) 127 | if err != nil { 128 | return &Error{"Failed to read note content", err} 129 | } 130 | 131 | // remove YAML frontmatter 132 | contentStr := string(content) 133 | if strings.HasPrefix(contentStr, "---") { 134 | if idx := strings.Index(contentStr[3:], "---"); idx != -1 { 135 | contentStr = contentStr[idx+6:] 136 | } 137 | } 138 | 139 | htmlContent, linkIDs, err := b.processContent(data.ID, contentStr, notes) 140 | if err != nil { 141 | return &Error{"Failed to process note content", err} 142 | } 143 | 144 | linkIDsJSON, err := json.Marshal(linkIDs) 145 | if err != nil { 146 | return errors.New("failed to marshal link IDs") 147 | } 148 | 149 | if data.Description == "" { 150 | htmlContentStripped := strings.ReplaceAll(strip.StripTags(htmlContent), "\n", "") 151 | data.Description = string([]rune(htmlContentStripped)[:common.MinInt(len([]rune(htmlContentStripped)), 100)]) 152 | } 153 | 154 | note := &common.Note{ 155 | ID: data.ID, 156 | Title: data.Title, 157 | Description: data.Description, 158 | HTMLContent: htmlContent, 159 | CreatedAt: data.CreatedAt, 160 | LastUpdatedAt: data.LastUpdatedAt, 161 | LinkNoteIDs: string(linkIDsJSON), 162 | } 163 | 164 | if err := b.db.InsertNote(note); err != nil { 165 | return &Error{"Failed to insert note into database", err} 166 | } 167 | } 168 | 169 | return nil 170 | } 171 | 172 | // processContent process note markdown to html and convert link 173 | func (b *ObsidianBuilder) processContent(noteID string, content string, notes map[string]*NoteData) (string, []string, error) { 174 | linkIDs := make([]string, 0) 175 | processed := b.processLinks(noteID, content, notes, &linkIDs) 176 | 177 | htmlContent, err := b.markdownToHTML(processed) 178 | if err != nil { 179 | return "", nil, err 180 | } 181 | 182 | return htmlContent, linkIDs, nil 183 | } 184 | 185 | // processLinks process [[link]] syntax 186 | func (b *ObsidianBuilder) processLinks(noteID string, content string, notes map[string]*NoteData, linkIDs *[]string) string { 187 | re := regexp.MustCompile(`\[\[([^\[\]]+)\]\]`) 188 | 189 | return re.ReplaceAllStringFunc(content, func(match string) string { 190 | inner := match[2 : len(match)-2] 191 | name, display := b.parseLinkParts(inner) 192 | 193 | if note, ok := notes[name]; ok { 194 | *linkIDs = append(*linkIDs, note.ID) 195 | return b.createLinkHTML(noteID, note.ID, display) 196 | } 197 | 198 | return display 199 | }) 200 | } 201 | 202 | // parseLinkParts parse double [] link and alias 203 | func (b *ObsidianBuilder) parseLinkParts(content string) (name string, display string) { 204 | parts := strings.Split(content, "|") 205 | 206 | if len(parts) == 1 { 207 | name = filepath.Base(parts[0]) 208 | return strings.TrimSpace(name), strings.TrimSpace(name) 209 | } 210 | 211 | name = filepath.Base(parts[0]) 212 | return strings.TrimSpace(name), strings.TrimSpace(parts[1]) 213 | } 214 | 215 | // createLinkHTML create htmx link 216 | func (b *ObsidianBuilder) createLinkHTML(fromID string, toID string, display string) string { 217 | return fmt.Sprintf(`%s`, 220 | toID, toID, fromID, fromID, display) 221 | } 222 | 223 | // getNoteMetadata read note content meta data 224 | func (b *ObsidianBuilder) getNoteMetadata(path string) (*NoteData, error) { 225 | content, err := os.ReadFile(path) 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | info, err := os.Stat(path) 231 | if err != nil { 232 | return nil, err 233 | } 234 | 235 | filename := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) 236 | 237 | note := &NoteData{ 238 | Title: filename, 239 | FilePath: path, 240 | CreatedAt: info.ModTime().Format("2006-01-02"), 241 | LastUpdatedAt: info.ModTime().Format("2006-01-02"), 242 | } 243 | 244 | if bytes.HasPrefix(content, []byte("---")) { 245 | if idx := bytes.Index(content[3:], []byte("---")); idx != -1 { 246 | var meta struct { 247 | Title string `yaml:"title"` 248 | Description string `yaml:"description"` 249 | CreatedAt string `yaml:"created_at"` 250 | LastUpdatedAt string `yaml:"last_updated_at"` 251 | } 252 | // If note contains metadata, then use the custom metadata. 253 | if err := yaml.Unmarshal(content[3:idx+3], &meta); err == nil { 254 | if meta.Title != "" { 255 | note.Title = meta.Title 256 | } 257 | if meta.Description != "" { 258 | note.Description = meta.Description 259 | } 260 | if meta.CreatedAt != "" { 261 | note.CreatedAt = meta.CreatedAt 262 | } 263 | if meta.LastUpdatedAt != "" { 264 | note.LastUpdatedAt = meta.LastUpdatedAt 265 | } 266 | } 267 | } 268 | } 269 | 270 | // Generate note id by filename without file ext. 271 | note.ID = b.idGenerator.Generate(filename) 272 | return note, nil 273 | } 274 | 275 | func (b *ObsidianBuilder) resolveHomePath(path string) string { 276 | if strings.HasPrefix(path, "~") { 277 | home, err := os.UserHomeDir() 278 | if err != nil { 279 | return path 280 | } 281 | return filepath.Join(home, path[1:]) 282 | } 283 | return path 284 | } 285 | 286 | func (b *ObsidianBuilder) markdownToHTML(content string) (string, error) { 287 | var buf bytes.Buffer 288 | if err := b.md.Convert([]byte(content), &buf); err != nil { 289 | return "", err 290 | } 291 | return buf.String(), nil 292 | } 293 | -------------------------------------------------------------------------------- /cli/builder/obsidian_test.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/blackstorm/markdownbrain/common" 9 | ) 10 | 11 | func TestObsidianBuilder(t *testing.T) { 12 | tmpDir, err := os.MkdirTemp("", "notes-test") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | defer os.RemoveAll(tmpDir) 17 | 18 | dbPath := filepath.Join(tmpDir, "test.db") 19 | db, err := common.NewDB(dbPath, true) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | defer db.Close() 24 | 25 | noteContent := `--- 26 | title: Test Note 27 | created_at: 2024-01-01 28 | --- 29 | # Test Note 30 | 31 | This is a test note with a [[link]] to another note.` 32 | 33 | notePath := filepath.Join(tmpDir, "test.md") 34 | if err := os.WriteFile(notePath, []byte(noteContent), 0644); err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | builder := NewObsidianBuilder([]string{".git"}, db) 39 | 40 | if err := builder.Build(tmpDir); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /cli/config.yml.example: -------------------------------------------------------------------------------- 1 | source: "~/Library/Mobile Documents/com~apple~CloudDocs/obsidian/example" 2 | server: "http://localhost:3000" 3 | api_key: "1234567890" 4 | ignores: 5 | - "Templates" 6 | -------------------------------------------------------------------------------- /cli/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Source string `yaml:"source"` 5 | Ignores []string `yaml:"ignores"` 6 | Server string `yaml:"server"` 7 | APIKey string `yaml:"api_key"` 8 | } 9 | -------------------------------------------------------------------------------- /cli/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blackstorm/markdownbrain/cli 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/go-resty/resty/v2 v2.16.2 7 | github.com/grokify/html-strip-tags-go v0.1.0 8 | github.com/yuin/goldmark v1.7.8 9 | gopkg.in/yaml.v3 v3.0.1 10 | ) 11 | 12 | require ( 13 | golang.org/x/net v0.27.0 // indirect 14 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 15 | ) 16 | 17 | replace github.com/blockstorm/markdownbrain/common => ../common 18 | -------------------------------------------------------------------------------- /cli/go.sum: -------------------------------------------------------------------------------- 1 | github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg= 2 | github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= 3 | github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= 4 | github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= 5 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 6 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= 11 | github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 12 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 13 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 14 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 17 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 18 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 19 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | 12 | "github.com/blackstorm/markdownbrain/cli/builder" 13 | "github.com/blackstorm/markdownbrain/cli/config" 14 | "github.com/blackstorm/markdownbrain/common" 15 | "github.com/go-resty/resty/v2" 16 | ) 17 | 18 | type Args struct { 19 | ConfigPath string 20 | } 21 | 22 | func main() { 23 | log.SetFlags(log.LstdFlags | log.Lshortfile) 24 | 25 | args, err := parseArgs() 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | config, err := loadConfig(args.ConfigPath) 31 | if err != nil { 32 | log.Fatalf("Failed to load config: %v", err) 33 | } 34 | 35 | db, err := common.NewDBWithTempDir() 36 | if err != nil { 37 | log.Fatalf("Failed to create database: %v", err) 38 | } 39 | log.Printf("Temporary database at: %s", db.Path) 40 | 41 | // 构建数据库 42 | log.Printf("Building database...") 43 | buildStart := time.Now() 44 | builder := builder.NewObsidianBuilder(mixIgnores(config), db) 45 | if err := builder.Build(config.Source); err != nil { 46 | log.Fatalf("Failed to build database: %v", err) 47 | } 48 | log.Printf("Database built in %v", time.Since(buildStart)) 49 | 50 | // 同步到服务器 51 | log.Printf("Syncing to server...") 52 | syncStart := time.Now() 53 | if err := syncToServer(config, db); err != nil { 54 | log.Fatalf("Failed to sync to server: %v", err) 55 | } 56 | log.Printf("Synced to server in %v", time.Since(syncStart)) 57 | } 58 | 59 | func mixIgnores(config *config.Config) []string { 60 | ignores := make(map[string]bool) 61 | defaultIgnores := []string{".git", ".obsidian", ".DS_Store"} 62 | for _, ignore := range defaultIgnores { 63 | ignores[ignore] = true 64 | } 65 | for _, ignore := range config.Ignores { 66 | ignores[ignore] = true 67 | } 68 | 69 | res := make([]string, 0) 70 | for item, ok := range ignores { 71 | if ok { 72 | res = append(res, item) 73 | } 74 | } 75 | 76 | return res 77 | } 78 | 79 | func syncToServer(config *config.Config, db *common.DB) error { 80 | client := resty.New() 81 | 82 | content, err := os.ReadFile(db.Path) 83 | if err != nil { 84 | return fmt.Errorf("failed to read database file: %w", err) 85 | } 86 | 87 | resp, err := client.R(). 88 | SetHeader("Authorization", fmt.Sprintf("Bearer %s", config.APIKey)). 89 | SetFileReader("db", "notes.db", bytes.NewReader(content)). 90 | Post(fmt.Sprintf("%s/api/sync", config.Server)) 91 | 92 | if err != nil { 93 | return fmt.Errorf("failed to send request: %w", err) 94 | } 95 | 96 | // 检查响应状态 97 | if !resp.IsSuccess() { 98 | return fmt.Errorf("server returned error status %d: %s", resp.StatusCode(), resp.String()) 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func parseArgs() (*Args, error) { 105 | homeDir, err := os.UserHomeDir() 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | path := filepath.Join(homeDir, "/markdownbrain/config.yml") 111 | 112 | var args Args 113 | flag.StringVar(&args.ConfigPath, "config", path, "CLI config path.") 114 | 115 | flag.Parse() 116 | 117 | return &args, nil 118 | } 119 | 120 | func loadConfig(path string) (*config.Config, error) { 121 | if path[:2] == "~/" { 122 | homeDir, err := os.UserHomeDir() 123 | if err != nil { 124 | return nil, fmt.Errorf("failed to get home directory: %w", err) 125 | } 126 | path = filepath.Join(homeDir, path[2:]) 127 | } 128 | 129 | var conf config.Config 130 | 131 | if err := common.ParseYAMLConfig(path, &conf); err != nil { 132 | return nil, err 133 | } 134 | 135 | return &conf, nil 136 | } 137 | -------------------------------------------------------------------------------- /common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "os" 5 | 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | func ParseYAMLConfig(filePath string, config any) error { 10 | content, err := os.ReadFile(filePath) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | if err := yaml.Unmarshal(content, config); err != nil { 16 | return err 17 | } 18 | 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /common/db.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/jmoiron/sqlx" 13 | _ "modernc.org/sqlite" 14 | ) 15 | 16 | var _DRIVER_NAME = "sqlite" 17 | 18 | var _SCHEMA = ` 19 | CREATE TABLE IF NOT EXISTS notes ( 20 | id TEXT PRIMARY KEY, 21 | title TEXT, 22 | description TEXT, 23 | html_content TEXT, 24 | created_at TEXT, 25 | last_updated_at TEXT, 26 | link_note_ids TEXT 27 | ) 28 | ` 29 | 30 | type DB struct { 31 | Path string 32 | connStr string 33 | pool atomic.Pointer[sqlx.DB] 34 | } 35 | 36 | func NewDBWithTempDir() (*DB, error) { 37 | tempDir := os.TempDir() 38 | tempAt := time.Now().Unix() 39 | path := filepath.Join(tempDir, fmt.Sprintf("temp_%d.db", tempAt)) 40 | 41 | return NewDB(path, false) 42 | } 43 | 44 | func NewDB(path string, readonly bool) (*DB, error) { 45 | if path == "" { 46 | return nil, errors.New("path is required") 47 | } 48 | 49 | mode := "rwc" 50 | 51 | // Create db file if not exists 52 | if _, err := os.Stat(path); err != nil { 53 | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 54 | return nil, fmt.Errorf("failed to create database directory: %v", err) 55 | } 56 | 57 | connStr := fmt.Sprintf("file:%s?cache=shared&mode=%s", path, mode) 58 | sqlxDB, err := sqlx.Connect(_DRIVER_NAME, connStr) 59 | if err != nil { 60 | return nil, errors.New("failed to create readonly db") 61 | } 62 | 63 | _, err = sqlxDB.Exec(_SCHEMA) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | if err := sqlxDB.Close(); err != nil { 69 | return nil, err 70 | } 71 | } 72 | 73 | args := "" 74 | if readonly { 75 | mode = "ro" 76 | args = "immutable=1&_query_only=1&_journal_mode=OFF" 77 | } 78 | 79 | connStr := fmt.Sprintf("file:%s?cache=shared&mode=%s&%s", path, mode, args) 80 | 81 | sqlxDB, err := sqlx.Connect(_DRIVER_NAME, connStr) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | db := &DB{ 87 | Path: path, 88 | connStr: connStr, 89 | } 90 | 91 | db.pool.Store(sqlxDB) 92 | 93 | return db, nil 94 | } 95 | 96 | func (db *DB) Close() error { 97 | return db.pool.Load().Close() 98 | } 99 | 100 | func (db *DB) FromBytes(bytes []byte) error { 101 | // Get current connection pool 102 | oldPool := db.pool.Load() 103 | 104 | // Close old connection pool 105 | if oldPool != nil { 106 | oldPool.Close() 107 | } 108 | 109 | // Remove old file 110 | if _, err := os.Stat(db.Path); err == nil { 111 | if err := os.Remove(db.Path); err != nil { 112 | return err 113 | } 114 | } 115 | 116 | // Write new file 117 | if err := os.WriteFile(db.Path, bytes, 0644); err != nil { 118 | return err 119 | } 120 | 121 | // Create new connection pool 122 | newPool, err := sqlx.Connect(_DRIVER_NAME, db.connStr) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | // Atomically replace the connection pool 128 | db.pool.Store(newPool) 129 | return nil 130 | } 131 | 132 | // Helper method: get current connection pool 133 | func (db *DB) getPool() *sqlx.DB { 134 | return db.pool.Load() 135 | } 136 | 137 | func (db *DB) InsertNote(note *Note) error { 138 | _, err := db.getPool().NamedExec(` 139 | INSERT INTO notes (id, title, description, html_content, created_at, last_updated_at, link_note_ids) 140 | VALUES (:id, :title, :description, :html_content, :created_at, :last_updated_at, :link_note_ids)`, 141 | note, 142 | ) 143 | return err 144 | } 145 | 146 | func (db *DB) GetNote(id string) (*Note, error) { 147 | note := &Note{} 148 | err := db.getPool().Get(note, "SELECT * FROM notes WHERE id = ?", id) 149 | if err != nil { 150 | if errors.Is(err, sql.ErrNoRows) { 151 | return nil, nil 152 | } 153 | return nil, err 154 | } 155 | return note, nil 156 | } 157 | 158 | func (db *DB) CountNote() (int64, error) { 159 | var count int64 160 | err := db.getPool().Get(&count, "SELECT COUNT(*) FROM notes") 161 | if err != nil { 162 | return 0, err 163 | } 164 | return count, nil 165 | } 166 | 167 | func (db *DB) GetNotesByIDs(ids []string) ([]Note, error) { 168 | query, args, err := sqlx.In("SELECT * FROM notes WHERE id IN (?)", ids) 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | // http://jmoiron.github.io/sqlx/#inQueries 174 | // sqlx.In returns queries with the MySQL placeholder (?), we need to rebind it 175 | // for SQLite 176 | query = db.getPool().Rebind(query) 177 | 178 | notes := []Note{} 179 | err = db.getPool().Select(¬es, query, args...) 180 | if err != nil { 181 | return nil, err 182 | } 183 | 184 | return notes, nil 185 | } 186 | 187 | func (db *DB) GetNotesByLinkTo(id string) ([]Note, error) { 188 | notes := []Note{} 189 | err := db.getPool().Select(¬es, "SELECT * FROM notes WHERE EXISTS (SELECT 1 FROM json_each(link_note_ids) WHERE value = ?)", id) 190 | if err != nil { 191 | return nil, err 192 | } 193 | 194 | return notes, nil 195 | } 196 | -------------------------------------------------------------------------------- /common/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blackstorm/markdownbrain/common 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/jmoiron/sqlx v1.4.0 7 | github.com/sqids/sqids-go v0.4.1 8 | gopkg.in/yaml.v3 v3.0.1 9 | modernc.org/sqlite v1.34.4 10 | ) 11 | 12 | require ( 13 | github.com/dustin/go-humanize v1.0.1 // indirect 14 | github.com/google/uuid v1.6.0 // indirect 15 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 16 | github.com/mattn/go-isatty v0.0.20 // indirect 17 | github.com/ncruces/go-strftime v0.1.9 // indirect 18 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 19 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect 20 | golang.org/x/sys v0.28.0 // indirect 21 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 22 | modernc.org/gc/v3 v3.0.0-20241223112719-96e2e1e4408d // indirect 23 | modernc.org/libc v1.61.5 // indirect 24 | modernc.org/mathutil v1.7.1 // indirect 25 | modernc.org/memory v1.8.0 // indirect 26 | modernc.org/strutil v1.2.1 // indirect 27 | modernc.org/token v1.1.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /common/go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 4 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 5 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 6 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 7 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= 8 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 12 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 13 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 14 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 15 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 16 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 17 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 18 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 21 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 22 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 23 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 24 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 25 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 26 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 27 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 31 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 32 | github.com/sqids/sqids-go v0.4.1 h1:eQKYzmAZbLlRwHeHYPF35QhgxwZHLnlmVj9AkIj/rrw= 33 | github.com/sqids/sqids-go v0.4.1/go.mod h1:EMwHuPQgSNFS0A49jESTfIQS+066XQTVhukrzEPScl8= 34 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= 35 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= 36 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 37 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 38 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 39 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 40 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 42 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 43 | golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= 44 | golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 47 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 48 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 49 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | modernc.org/cc/v4 v4.24.1 h1:mLykA8iIlZ/SZbwI2JgYIURXQMSgmOb/+5jaielxPi4= 51 | modernc.org/cc/v4 v4.24.1/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI= 52 | modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw= 53 | modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ= 54 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 55 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 56 | modernc.org/gc/v2 v2.6.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4= 57 | modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= 58 | modernc.org/gc/v3 v3.0.0-20241223112719-96e2e1e4408d h1:d0JExN5U5FjUVHCP6L9DIlLJBZveR6KUM4AvfDUL3+k= 59 | modernc.org/gc/v3 v3.0.0-20241223112719-96e2e1e4408d/go.mod h1:qBSLm/exCqouT2hrfyTKikWKG9IPq8EoX5fS00l3jqk= 60 | modernc.org/libc v1.61.5 h1:WzsPUvWl2CvsRmk2foyWWHUEUmQ2iW4oFyWOVR0O5ho= 61 | modernc.org/libc v1.61.5/go.mod h1:llBdEGIywhnRgAFuTF+CWaKV8/2bFgACcQZTXhkAuAM= 62 | modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 63 | modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 64 | modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= 65 | modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 66 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 67 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 68 | modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= 69 | modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= 70 | modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8= 71 | modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk= 72 | modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 73 | modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= 74 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 75 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 76 | -------------------------------------------------------------------------------- /common/id.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "hash/crc32" 5 | 6 | "github.com/sqids/sqids-go" 7 | ) 8 | 9 | const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 10 | 11 | type IdGenerator struct { 12 | sqids *sqids.Sqids 13 | } 14 | 15 | func NewSqidsIdGenerator() *IdGenerator { 16 | s, _ := sqids.New(sqids.Options{ 17 | Alphabet: alphabet, 18 | }) 19 | return &IdGenerator{sqids: s} 20 | } 21 | 22 | func (g *IdGenerator) Generate(data string) string { 23 | crc := crc32.ChecksumIEEE([]byte(data)) 24 | id, _ := g.sqids.Encode([]uint64{uint64(crc)}) 25 | return id 26 | } 27 | -------------------------------------------------------------------------------- /common/id_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "testing" 4 | 5 | func TestGenerator(t *testing.T) { 6 | generator := NewSqidsIdGenerator() 7 | 8 | tests := []struct { 9 | input string 10 | expected string 11 | }{ 12 | {"welcome", "nWd9WFI"}, 13 | {"欢迎", "Q6YVebZ"}, 14 | {"bienvenue", "psxRKiT"}, 15 | {"むかえる", "4thJ4Q7"}, 16 | } 17 | 18 | for _, tt := range tests { 19 | result := generator.Generate(tt.input) 20 | if result != tt.expected { 21 | t.Errorf("Generate(%s) = %s; want %s", tt.input, result, tt.expected) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /common/note.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type Note struct { 8 | ID string `json:"id" db:"id"` 9 | Title string `json:"title" db:"title"` 10 | Description string `json:"description" db:"description"` 11 | HTMLContent string `json:"html_content" db:"html_content"` 12 | CreatedAt string `json:"created_at" db:"created_at"` 13 | LastUpdatedAt string `json:"last_updated_at" db:"last_updated_at"` 14 | LinkNoteIDs string `json:"link_note_ids" db:"link_note_ids"` 15 | LinkToThis []Note 16 | } 17 | 18 | func (n *Note) LoadLinkToThisNotes(db *DB) error { 19 | linkToThis, err := db.GetNotesByLinkTo(n.ID) 20 | if err != nil { 21 | return err 22 | } 23 | n.LinkToThis = linkToThis 24 | return nil 25 | } 26 | 27 | type Notes []Note 28 | 29 | func (n Notes) Titles() string { 30 | titles := make([]string, len(n)) 31 | for i, note := range n { 32 | titles[i] = note.Title 33 | } 34 | return strings.Join(titles, "|") 35 | } 36 | -------------------------------------------------------------------------------- /common/urils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | func MinInt(x, y int) int { 4 | if x < y { 5 | return x 6 | } 7 | return y 8 | } 9 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.23.4 2 | 3 | use ( 4 | ./cli 5 | ./common 6 | ./server 7 | ) 8 | -------------------------------------------------------------------------------- /go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 4 | github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= 5 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 6 | github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= 7 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 8 | golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= 9 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 10 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 11 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 12 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 13 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 14 | lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 15 | modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= 16 | modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= 17 | modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= 18 | modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= 19 | modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdownbrain", 3 | "version": "0.0.1", 4 | "author": "blackstorm", 5 | "license": "MIT", 6 | "description": "markdownbrain", 7 | "scripts": { 8 | "dev": "pnpx tailwindcss -i ./tailwind.css -o ./server/www/static/app.css --watch", 9 | "build": "pnpx tailwindcss -i ./tailwind.css -o ./server/www/static/app.css --minify" 10 | }, 11 | "devDependencies": { 12 | "@tailwindcss/typography": "^0.5.15", 13 | "tailwindcss": "^3.4.15" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@tailwindcss/typography': 12 | specifier: ^0.5.15 13 | version: 0.5.15(tailwindcss@3.4.17) 14 | tailwindcss: 15 | specifier: ^3.4.15 16 | version: 3.4.17 17 | 18 | packages: 19 | 20 | '@alloc/quick-lru@5.2.0': 21 | resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 22 | engines: {node: '>=10'} 23 | 24 | '@isaacs/cliui@8.0.2': 25 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 26 | engines: {node: '>=12'} 27 | 28 | '@jridgewell/gen-mapping@0.3.8': 29 | resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} 30 | engines: {node: '>=6.0.0'} 31 | 32 | '@jridgewell/resolve-uri@3.1.2': 33 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 34 | engines: {node: '>=6.0.0'} 35 | 36 | '@jridgewell/set-array@1.2.1': 37 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 38 | engines: {node: '>=6.0.0'} 39 | 40 | '@jridgewell/sourcemap-codec@1.5.0': 41 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 42 | 43 | '@jridgewell/trace-mapping@0.3.25': 44 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 45 | 46 | '@nodelib/fs.scandir@2.1.5': 47 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 48 | engines: {node: '>= 8'} 49 | 50 | '@nodelib/fs.stat@2.0.5': 51 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 52 | engines: {node: '>= 8'} 53 | 54 | '@nodelib/fs.walk@1.2.8': 55 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 56 | engines: {node: '>= 8'} 57 | 58 | '@pkgjs/parseargs@0.11.0': 59 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 60 | engines: {node: '>=14'} 61 | 62 | '@tailwindcss/typography@0.5.15': 63 | resolution: {integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==} 64 | peerDependencies: 65 | tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' 66 | 67 | ansi-regex@5.0.1: 68 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 69 | engines: {node: '>=8'} 70 | 71 | ansi-regex@6.1.0: 72 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 73 | engines: {node: '>=12'} 74 | 75 | ansi-styles@4.3.0: 76 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 77 | engines: {node: '>=8'} 78 | 79 | ansi-styles@6.2.1: 80 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 81 | engines: {node: '>=12'} 82 | 83 | any-promise@1.3.0: 84 | resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 85 | 86 | anymatch@3.1.3: 87 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 88 | engines: {node: '>= 8'} 89 | 90 | arg@5.0.2: 91 | resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 92 | 93 | balanced-match@1.0.2: 94 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 95 | 96 | binary-extensions@2.3.0: 97 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 98 | engines: {node: '>=8'} 99 | 100 | brace-expansion@2.0.1: 101 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 102 | 103 | braces@3.0.3: 104 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 105 | engines: {node: '>=8'} 106 | 107 | camelcase-css@2.0.1: 108 | resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 109 | engines: {node: '>= 6'} 110 | 111 | chokidar@3.6.0: 112 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 113 | engines: {node: '>= 8.10.0'} 114 | 115 | color-convert@2.0.1: 116 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 117 | engines: {node: '>=7.0.0'} 118 | 119 | color-name@1.1.4: 120 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 121 | 122 | commander@4.1.1: 123 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 124 | engines: {node: '>= 6'} 125 | 126 | cross-spawn@7.0.6: 127 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 128 | engines: {node: '>= 8'} 129 | 130 | cssesc@3.0.0: 131 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 132 | engines: {node: '>=4'} 133 | hasBin: true 134 | 135 | didyoumean@1.2.2: 136 | resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 137 | 138 | dlv@1.1.3: 139 | resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 140 | 141 | eastasianwidth@0.2.0: 142 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 143 | 144 | emoji-regex@8.0.0: 145 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 146 | 147 | emoji-regex@9.2.2: 148 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 149 | 150 | fast-glob@3.3.2: 151 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 152 | engines: {node: '>=8.6.0'} 153 | 154 | fastq@1.18.0: 155 | resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==} 156 | 157 | fill-range@7.1.1: 158 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 159 | engines: {node: '>=8'} 160 | 161 | foreground-child@3.3.0: 162 | resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} 163 | engines: {node: '>=14'} 164 | 165 | fsevents@2.3.3: 166 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 167 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 168 | os: [darwin] 169 | 170 | function-bind@1.1.2: 171 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 172 | 173 | glob-parent@5.1.2: 174 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 175 | engines: {node: '>= 6'} 176 | 177 | glob-parent@6.0.2: 178 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 179 | engines: {node: '>=10.13.0'} 180 | 181 | glob@10.4.5: 182 | resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 183 | hasBin: true 184 | 185 | hasown@2.0.2: 186 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 187 | engines: {node: '>= 0.4'} 188 | 189 | is-binary-path@2.1.0: 190 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 191 | engines: {node: '>=8'} 192 | 193 | is-core-module@2.16.1: 194 | resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} 195 | engines: {node: '>= 0.4'} 196 | 197 | is-extglob@2.1.1: 198 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 199 | engines: {node: '>=0.10.0'} 200 | 201 | is-fullwidth-code-point@3.0.0: 202 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 203 | engines: {node: '>=8'} 204 | 205 | is-glob@4.0.3: 206 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 207 | engines: {node: '>=0.10.0'} 208 | 209 | is-number@7.0.0: 210 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 211 | engines: {node: '>=0.12.0'} 212 | 213 | isexe@2.0.0: 214 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 215 | 216 | jackspeak@3.4.3: 217 | resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 218 | 219 | jiti@1.21.7: 220 | resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} 221 | hasBin: true 222 | 223 | lilconfig@3.1.3: 224 | resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 225 | engines: {node: '>=14'} 226 | 227 | lines-and-columns@1.2.4: 228 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 229 | 230 | lodash.castarray@4.4.0: 231 | resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} 232 | 233 | lodash.isplainobject@4.0.6: 234 | resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} 235 | 236 | lodash.merge@4.6.2: 237 | resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 238 | 239 | lru-cache@10.4.3: 240 | resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 241 | 242 | merge2@1.4.1: 243 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 244 | engines: {node: '>= 8'} 245 | 246 | micromatch@4.0.8: 247 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 248 | engines: {node: '>=8.6'} 249 | 250 | minimatch@9.0.5: 251 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 252 | engines: {node: '>=16 || 14 >=14.17'} 253 | 254 | minipass@7.1.2: 255 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 256 | engines: {node: '>=16 || 14 >=14.17'} 257 | 258 | mz@2.7.0: 259 | resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 260 | 261 | nanoid@3.3.8: 262 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 263 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 264 | hasBin: true 265 | 266 | normalize-path@3.0.0: 267 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 268 | engines: {node: '>=0.10.0'} 269 | 270 | object-assign@4.1.1: 271 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 272 | engines: {node: '>=0.10.0'} 273 | 274 | object-hash@3.0.0: 275 | resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 276 | engines: {node: '>= 6'} 277 | 278 | package-json-from-dist@1.0.1: 279 | resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 280 | 281 | path-key@3.1.1: 282 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 283 | engines: {node: '>=8'} 284 | 285 | path-parse@1.0.7: 286 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 287 | 288 | path-scurry@1.11.1: 289 | resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 290 | engines: {node: '>=16 || 14 >=14.18'} 291 | 292 | picocolors@1.1.1: 293 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 294 | 295 | picomatch@2.3.1: 296 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 297 | engines: {node: '>=8.6'} 298 | 299 | pify@2.3.0: 300 | resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 301 | engines: {node: '>=0.10.0'} 302 | 303 | pirates@4.0.6: 304 | resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 305 | engines: {node: '>= 6'} 306 | 307 | postcss-import@15.1.0: 308 | resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 309 | engines: {node: '>=14.0.0'} 310 | peerDependencies: 311 | postcss: ^8.0.0 312 | 313 | postcss-js@4.0.1: 314 | resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 315 | engines: {node: ^12 || ^14 || >= 16} 316 | peerDependencies: 317 | postcss: ^8.4.21 318 | 319 | postcss-load-config@4.0.2: 320 | resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} 321 | engines: {node: '>= 14'} 322 | peerDependencies: 323 | postcss: '>=8.0.9' 324 | ts-node: '>=9.0.0' 325 | peerDependenciesMeta: 326 | postcss: 327 | optional: true 328 | ts-node: 329 | optional: true 330 | 331 | postcss-nested@6.2.0: 332 | resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} 333 | engines: {node: '>=12.0'} 334 | peerDependencies: 335 | postcss: ^8.2.14 336 | 337 | postcss-selector-parser@6.0.10: 338 | resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} 339 | engines: {node: '>=4'} 340 | 341 | postcss-selector-parser@6.1.2: 342 | resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} 343 | engines: {node: '>=4'} 344 | 345 | postcss-value-parser@4.2.0: 346 | resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 347 | 348 | postcss@8.4.49: 349 | resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} 350 | engines: {node: ^10 || ^12 || >=14} 351 | 352 | queue-microtask@1.2.3: 353 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 354 | 355 | read-cache@1.0.0: 356 | resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 357 | 358 | readdirp@3.6.0: 359 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 360 | engines: {node: '>=8.10.0'} 361 | 362 | resolve@1.22.10: 363 | resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} 364 | engines: {node: '>= 0.4'} 365 | hasBin: true 366 | 367 | reusify@1.0.4: 368 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 369 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 370 | 371 | run-parallel@1.2.0: 372 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 373 | 374 | shebang-command@2.0.0: 375 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 376 | engines: {node: '>=8'} 377 | 378 | shebang-regex@3.0.0: 379 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 380 | engines: {node: '>=8'} 381 | 382 | signal-exit@4.1.0: 383 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 384 | engines: {node: '>=14'} 385 | 386 | source-map-js@1.2.1: 387 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 388 | engines: {node: '>=0.10.0'} 389 | 390 | string-width@4.2.3: 391 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 392 | engines: {node: '>=8'} 393 | 394 | string-width@5.1.2: 395 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 396 | engines: {node: '>=12'} 397 | 398 | strip-ansi@6.0.1: 399 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 400 | engines: {node: '>=8'} 401 | 402 | strip-ansi@7.1.0: 403 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 404 | engines: {node: '>=12'} 405 | 406 | sucrase@3.35.0: 407 | resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 408 | engines: {node: '>=16 || 14 >=14.17'} 409 | hasBin: true 410 | 411 | supports-preserve-symlinks-flag@1.0.0: 412 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 413 | engines: {node: '>= 0.4'} 414 | 415 | tailwindcss@3.4.17: 416 | resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} 417 | engines: {node: '>=14.0.0'} 418 | hasBin: true 419 | 420 | thenify-all@1.6.0: 421 | resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 422 | engines: {node: '>=0.8'} 423 | 424 | thenify@3.3.1: 425 | resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 426 | 427 | to-regex-range@5.0.1: 428 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 429 | engines: {node: '>=8.0'} 430 | 431 | ts-interface-checker@0.1.13: 432 | resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 433 | 434 | util-deprecate@1.0.2: 435 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 436 | 437 | which@2.0.2: 438 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 439 | engines: {node: '>= 8'} 440 | hasBin: true 441 | 442 | wrap-ansi@7.0.0: 443 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 444 | engines: {node: '>=10'} 445 | 446 | wrap-ansi@8.1.0: 447 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 448 | engines: {node: '>=12'} 449 | 450 | yaml@2.6.1: 451 | resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} 452 | engines: {node: '>= 14'} 453 | hasBin: true 454 | 455 | snapshots: 456 | 457 | '@alloc/quick-lru@5.2.0': {} 458 | 459 | '@isaacs/cliui@8.0.2': 460 | dependencies: 461 | string-width: 5.1.2 462 | string-width-cjs: string-width@4.2.3 463 | strip-ansi: 7.1.0 464 | strip-ansi-cjs: strip-ansi@6.0.1 465 | wrap-ansi: 8.1.0 466 | wrap-ansi-cjs: wrap-ansi@7.0.0 467 | 468 | '@jridgewell/gen-mapping@0.3.8': 469 | dependencies: 470 | '@jridgewell/set-array': 1.2.1 471 | '@jridgewell/sourcemap-codec': 1.5.0 472 | '@jridgewell/trace-mapping': 0.3.25 473 | 474 | '@jridgewell/resolve-uri@3.1.2': {} 475 | 476 | '@jridgewell/set-array@1.2.1': {} 477 | 478 | '@jridgewell/sourcemap-codec@1.5.0': {} 479 | 480 | '@jridgewell/trace-mapping@0.3.25': 481 | dependencies: 482 | '@jridgewell/resolve-uri': 3.1.2 483 | '@jridgewell/sourcemap-codec': 1.5.0 484 | 485 | '@nodelib/fs.scandir@2.1.5': 486 | dependencies: 487 | '@nodelib/fs.stat': 2.0.5 488 | run-parallel: 1.2.0 489 | 490 | '@nodelib/fs.stat@2.0.5': {} 491 | 492 | '@nodelib/fs.walk@1.2.8': 493 | dependencies: 494 | '@nodelib/fs.scandir': 2.1.5 495 | fastq: 1.18.0 496 | 497 | '@pkgjs/parseargs@0.11.0': 498 | optional: true 499 | 500 | '@tailwindcss/typography@0.5.15(tailwindcss@3.4.17)': 501 | dependencies: 502 | lodash.castarray: 4.4.0 503 | lodash.isplainobject: 4.0.6 504 | lodash.merge: 4.6.2 505 | postcss-selector-parser: 6.0.10 506 | tailwindcss: 3.4.17 507 | 508 | ansi-regex@5.0.1: {} 509 | 510 | ansi-regex@6.1.0: {} 511 | 512 | ansi-styles@4.3.0: 513 | dependencies: 514 | color-convert: 2.0.1 515 | 516 | ansi-styles@6.2.1: {} 517 | 518 | any-promise@1.3.0: {} 519 | 520 | anymatch@3.1.3: 521 | dependencies: 522 | normalize-path: 3.0.0 523 | picomatch: 2.3.1 524 | 525 | arg@5.0.2: {} 526 | 527 | balanced-match@1.0.2: {} 528 | 529 | binary-extensions@2.3.0: {} 530 | 531 | brace-expansion@2.0.1: 532 | dependencies: 533 | balanced-match: 1.0.2 534 | 535 | braces@3.0.3: 536 | dependencies: 537 | fill-range: 7.1.1 538 | 539 | camelcase-css@2.0.1: {} 540 | 541 | chokidar@3.6.0: 542 | dependencies: 543 | anymatch: 3.1.3 544 | braces: 3.0.3 545 | glob-parent: 5.1.2 546 | is-binary-path: 2.1.0 547 | is-glob: 4.0.3 548 | normalize-path: 3.0.0 549 | readdirp: 3.6.0 550 | optionalDependencies: 551 | fsevents: 2.3.3 552 | 553 | color-convert@2.0.1: 554 | dependencies: 555 | color-name: 1.1.4 556 | 557 | color-name@1.1.4: {} 558 | 559 | commander@4.1.1: {} 560 | 561 | cross-spawn@7.0.6: 562 | dependencies: 563 | path-key: 3.1.1 564 | shebang-command: 2.0.0 565 | which: 2.0.2 566 | 567 | cssesc@3.0.0: {} 568 | 569 | didyoumean@1.2.2: {} 570 | 571 | dlv@1.1.3: {} 572 | 573 | eastasianwidth@0.2.0: {} 574 | 575 | emoji-regex@8.0.0: {} 576 | 577 | emoji-regex@9.2.2: {} 578 | 579 | fast-glob@3.3.2: 580 | dependencies: 581 | '@nodelib/fs.stat': 2.0.5 582 | '@nodelib/fs.walk': 1.2.8 583 | glob-parent: 5.1.2 584 | merge2: 1.4.1 585 | micromatch: 4.0.8 586 | 587 | fastq@1.18.0: 588 | dependencies: 589 | reusify: 1.0.4 590 | 591 | fill-range@7.1.1: 592 | dependencies: 593 | to-regex-range: 5.0.1 594 | 595 | foreground-child@3.3.0: 596 | dependencies: 597 | cross-spawn: 7.0.6 598 | signal-exit: 4.1.0 599 | 600 | fsevents@2.3.3: 601 | optional: true 602 | 603 | function-bind@1.1.2: {} 604 | 605 | glob-parent@5.1.2: 606 | dependencies: 607 | is-glob: 4.0.3 608 | 609 | glob-parent@6.0.2: 610 | dependencies: 611 | is-glob: 4.0.3 612 | 613 | glob@10.4.5: 614 | dependencies: 615 | foreground-child: 3.3.0 616 | jackspeak: 3.4.3 617 | minimatch: 9.0.5 618 | minipass: 7.1.2 619 | package-json-from-dist: 1.0.1 620 | path-scurry: 1.11.1 621 | 622 | hasown@2.0.2: 623 | dependencies: 624 | function-bind: 1.1.2 625 | 626 | is-binary-path@2.1.0: 627 | dependencies: 628 | binary-extensions: 2.3.0 629 | 630 | is-core-module@2.16.1: 631 | dependencies: 632 | hasown: 2.0.2 633 | 634 | is-extglob@2.1.1: {} 635 | 636 | is-fullwidth-code-point@3.0.0: {} 637 | 638 | is-glob@4.0.3: 639 | dependencies: 640 | is-extglob: 2.1.1 641 | 642 | is-number@7.0.0: {} 643 | 644 | isexe@2.0.0: {} 645 | 646 | jackspeak@3.4.3: 647 | dependencies: 648 | '@isaacs/cliui': 8.0.2 649 | optionalDependencies: 650 | '@pkgjs/parseargs': 0.11.0 651 | 652 | jiti@1.21.7: {} 653 | 654 | lilconfig@3.1.3: {} 655 | 656 | lines-and-columns@1.2.4: {} 657 | 658 | lodash.castarray@4.4.0: {} 659 | 660 | lodash.isplainobject@4.0.6: {} 661 | 662 | lodash.merge@4.6.2: {} 663 | 664 | lru-cache@10.4.3: {} 665 | 666 | merge2@1.4.1: {} 667 | 668 | micromatch@4.0.8: 669 | dependencies: 670 | braces: 3.0.3 671 | picomatch: 2.3.1 672 | 673 | minimatch@9.0.5: 674 | dependencies: 675 | brace-expansion: 2.0.1 676 | 677 | minipass@7.1.2: {} 678 | 679 | mz@2.7.0: 680 | dependencies: 681 | any-promise: 1.3.0 682 | object-assign: 4.1.1 683 | thenify-all: 1.6.0 684 | 685 | nanoid@3.3.8: {} 686 | 687 | normalize-path@3.0.0: {} 688 | 689 | object-assign@4.1.1: {} 690 | 691 | object-hash@3.0.0: {} 692 | 693 | package-json-from-dist@1.0.1: {} 694 | 695 | path-key@3.1.1: {} 696 | 697 | path-parse@1.0.7: {} 698 | 699 | path-scurry@1.11.1: 700 | dependencies: 701 | lru-cache: 10.4.3 702 | minipass: 7.1.2 703 | 704 | picocolors@1.1.1: {} 705 | 706 | picomatch@2.3.1: {} 707 | 708 | pify@2.3.0: {} 709 | 710 | pirates@4.0.6: {} 711 | 712 | postcss-import@15.1.0(postcss@8.4.49): 713 | dependencies: 714 | postcss: 8.4.49 715 | postcss-value-parser: 4.2.0 716 | read-cache: 1.0.0 717 | resolve: 1.22.10 718 | 719 | postcss-js@4.0.1(postcss@8.4.49): 720 | dependencies: 721 | camelcase-css: 2.0.1 722 | postcss: 8.4.49 723 | 724 | postcss-load-config@4.0.2(postcss@8.4.49): 725 | dependencies: 726 | lilconfig: 3.1.3 727 | yaml: 2.6.1 728 | optionalDependencies: 729 | postcss: 8.4.49 730 | 731 | postcss-nested@6.2.0(postcss@8.4.49): 732 | dependencies: 733 | postcss: 8.4.49 734 | postcss-selector-parser: 6.1.2 735 | 736 | postcss-selector-parser@6.0.10: 737 | dependencies: 738 | cssesc: 3.0.0 739 | util-deprecate: 1.0.2 740 | 741 | postcss-selector-parser@6.1.2: 742 | dependencies: 743 | cssesc: 3.0.0 744 | util-deprecate: 1.0.2 745 | 746 | postcss-value-parser@4.2.0: {} 747 | 748 | postcss@8.4.49: 749 | dependencies: 750 | nanoid: 3.3.8 751 | picocolors: 1.1.1 752 | source-map-js: 1.2.1 753 | 754 | queue-microtask@1.2.3: {} 755 | 756 | read-cache@1.0.0: 757 | dependencies: 758 | pify: 2.3.0 759 | 760 | readdirp@3.6.0: 761 | dependencies: 762 | picomatch: 2.3.1 763 | 764 | resolve@1.22.10: 765 | dependencies: 766 | is-core-module: 2.16.1 767 | path-parse: 1.0.7 768 | supports-preserve-symlinks-flag: 1.0.0 769 | 770 | reusify@1.0.4: {} 771 | 772 | run-parallel@1.2.0: 773 | dependencies: 774 | queue-microtask: 1.2.3 775 | 776 | shebang-command@2.0.0: 777 | dependencies: 778 | shebang-regex: 3.0.0 779 | 780 | shebang-regex@3.0.0: {} 781 | 782 | signal-exit@4.1.0: {} 783 | 784 | source-map-js@1.2.1: {} 785 | 786 | string-width@4.2.3: 787 | dependencies: 788 | emoji-regex: 8.0.0 789 | is-fullwidth-code-point: 3.0.0 790 | strip-ansi: 6.0.1 791 | 792 | string-width@5.1.2: 793 | dependencies: 794 | eastasianwidth: 0.2.0 795 | emoji-regex: 9.2.2 796 | strip-ansi: 7.1.0 797 | 798 | strip-ansi@6.0.1: 799 | dependencies: 800 | ansi-regex: 5.0.1 801 | 802 | strip-ansi@7.1.0: 803 | dependencies: 804 | ansi-regex: 6.1.0 805 | 806 | sucrase@3.35.0: 807 | dependencies: 808 | '@jridgewell/gen-mapping': 0.3.8 809 | commander: 4.1.1 810 | glob: 10.4.5 811 | lines-and-columns: 1.2.4 812 | mz: 2.7.0 813 | pirates: 4.0.6 814 | ts-interface-checker: 0.1.13 815 | 816 | supports-preserve-symlinks-flag@1.0.0: {} 817 | 818 | tailwindcss@3.4.17: 819 | dependencies: 820 | '@alloc/quick-lru': 5.2.0 821 | arg: 5.0.2 822 | chokidar: 3.6.0 823 | didyoumean: 1.2.2 824 | dlv: 1.1.3 825 | fast-glob: 3.3.2 826 | glob-parent: 6.0.2 827 | is-glob: 4.0.3 828 | jiti: 1.21.7 829 | lilconfig: 3.1.3 830 | micromatch: 4.0.8 831 | normalize-path: 3.0.0 832 | object-hash: 3.0.0 833 | picocolors: 1.1.1 834 | postcss: 8.4.49 835 | postcss-import: 15.1.0(postcss@8.4.49) 836 | postcss-js: 4.0.1(postcss@8.4.49) 837 | postcss-load-config: 4.0.2(postcss@8.4.49) 838 | postcss-nested: 6.2.0(postcss@8.4.49) 839 | postcss-selector-parser: 6.1.2 840 | resolve: 1.22.10 841 | sucrase: 3.35.0 842 | transitivePeerDependencies: 843 | - ts-node 844 | 845 | thenify-all@1.6.0: 846 | dependencies: 847 | thenify: 3.3.1 848 | 849 | thenify@3.3.1: 850 | dependencies: 851 | any-promise: 1.3.0 852 | 853 | to-regex-range@5.0.1: 854 | dependencies: 855 | is-number: 7.0.0 856 | 857 | ts-interface-checker@0.1.13: {} 858 | 859 | util-deprecate@1.0.2: {} 860 | 861 | which@2.0.2: 862 | dependencies: 863 | isexe: 2.0.0 864 | 865 | wrap-ansi@7.0.0: 866 | dependencies: 867 | ansi-styles: 4.3.0 868 | string-width: 4.2.3 869 | strip-ansi: 6.0.1 870 | 871 | wrap-ansi@8.1.0: 872 | dependencies: 873 | ansi-styles: 6.2.1 874 | string-width: 5.1.2 875 | strip-ansi: 7.1.0 876 | 877 | yaml@2.6.1: {} 878 | -------------------------------------------------------------------------------- /screenshots/markdownbrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackstorm/markdownbrain/c9ce7560c1f81c83c6698912a23f50923d492707/screenshots/markdownbrain.png -------------------------------------------------------------------------------- /server.config.example: -------------------------------------------------------------------------------- 1 | root_note_name: "Welcome" 2 | lang: "en" 3 | name: "MarkdownBrain" 4 | description: "MarkdownBrain" 5 | api_key: "1234567890" 6 | # Use CDN 7 | # htmx_js_url: "https://cdn.jsdelivr.net/npm/htmx.org@2.0.4/dist/htmx.min.js" 8 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | DEV_MODE=true 2 | PORT=3000 3 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | www/data/* 3 | !www/data/.keep -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23-bookworm AS builder 2 | 3 | WORKDIR /markdownbrain 4 | 5 | COPY . . 6 | 7 | RUN go build -o markdownbrain server/main.go 8 | 9 | FROM debian:bookworm 10 | 11 | WORKDIR /markdownbrain 12 | 13 | COPY --from=builder /markdownbrain/server/www/static /markdownbrain/static 14 | COPY --from=builder /markdownbrain/server/www/config.yml /markdownbrain/config.yml 15 | COPY --from=builder /markdownbrain/markdownbrain /markdownbrain/markdownbrain 16 | 17 | EXPOSE ${PORT:-3000} 18 | 19 | CMD ["./markdownbrain"] 20 | -------------------------------------------------------------------------------- /server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Name string `yaml:"name"` 5 | Description string `yaml:"description"` 6 | Lang string `yaml:"lang"` 7 | RootNoteName string `yaml:"root_note_name"` 8 | APIKey string `yaml:"api_key"` 9 | HtmxJsUrl string `yaml:"htmx_js_url"` 10 | Templates []string `yaml:"templates"` 11 | } 12 | -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blackstorm/markdownbrain/server 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/gofiber/fiber/v2 v2.52.5 7 | github.com/gofiber/template/django/v3 v3.1.12 8 | github.com/joho/godotenv v1.5.1 9 | ) 10 | 11 | require ( 12 | github.com/andybalholm/brotli v1.0.5 // indirect 13 | github.com/flosch/pongo2/v6 v6.0.0 // indirect 14 | github.com/gofiber/template v1.8.3 // indirect 15 | github.com/gofiber/utils v1.1.0 // indirect 16 | github.com/google/uuid v1.6.0 // indirect 17 | github.com/klauspost/compress v1.17.0 // indirect 18 | github.com/mattn/go-colorable v0.1.13 // indirect 19 | github.com/mattn/go-isatty v0.0.20 // indirect 20 | github.com/mattn/go-runewidth v0.0.15 // indirect 21 | github.com/rivo/uniseg v0.2.0 // indirect 22 | github.com/valyala/bytebufferpool v1.0.0 // indirect 23 | github.com/valyala/fasthttp v1.51.0 // indirect 24 | github.com/valyala/tcplisten v1.0.0 // indirect 25 | golang.org/x/sys v0.22.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /server/go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 2 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= 5 | github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= 6 | github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= 7 | github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= 8 | github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= 9 | github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= 10 | github.com/gofiber/template/django/v3 v3.1.12 h1:w2jxm9bJajhvrroXqEmUmakbvDSlzjpHgOI8yyh2iJs= 11 | github.com/gofiber/template/django/v3 v3.1.12/go.mod h1:4YNpM+LJ/el+cjUpdulp8lOH6dxZ2jaQmrF4E2/KGAk= 12 | github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= 13 | github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= 14 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 15 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 16 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 17 | github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= 18 | github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 19 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 20 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 21 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 22 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 23 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 24 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 25 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 26 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 27 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 30 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 31 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 32 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 33 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 34 | github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= 35 | github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= 36 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 37 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 38 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 41 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 42 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/blackstorm/markdownbrain/common" 15 | "github.com/blackstorm/markdownbrain/server/config" 16 | "github.com/gofiber/fiber/v2" 17 | "github.com/gofiber/fiber/v2/middleware/favicon" 18 | "github.com/gofiber/template/django/v3" 19 | _ "github.com/joho/godotenv/autoload" 20 | ) 21 | 22 | type AppState struct { 23 | db *common.DB 24 | config *config.Config 25 | rootNoteId string 26 | } 27 | 28 | //go:embed templates 29 | var templatesAssets embed.FS 30 | 31 | func main() { 32 | devMode := os.Getenv("DEV_MODE") == "true" 33 | 34 | workspace := "/markdownbrain" 35 | if devMode { 36 | currentDir, err := os.Getwd() 37 | if err != nil { 38 | panic(err) 39 | } 40 | workspace = filepath.Join(currentDir, "www") 41 | } 42 | 43 | configPath := filepath.Join(workspace, "config.yml") 44 | dbPath := filepath.Join(workspace, "/data/notes.db") 45 | 46 | log.Printf("Workspace path: %s", workspace) 47 | log.Printf("Config path: %s", configPath) 48 | log.Printf("Database path: %s", dbPath) 49 | 50 | config, err := loadConfig(configPath) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | db, err := common.NewDB(dbPath, true) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | rootNoteHashId := common.NewSqidsIdGenerator().Generate(config.RootNoteName) 61 | state := &AppState{ 62 | db: db, 63 | config: config, 64 | rootNoteId: rootNoteHashId, 65 | } 66 | 67 | // Use views example: django.New("./templates", ".html"), 68 | app := fiber.New(fiber.Config{ 69 | Views: django.NewPathForwardingFileSystem(http.FS(templatesAssets), "/templates", ".html"), 70 | }) 71 | 72 | app.Use(favicon.New(favicon.Config{ 73 | File: filepath.Join(workspace, "/static/favicon.ico"), 74 | URL: "/favicon.ico", 75 | })) 76 | 77 | app.Static("/static", filepath.Join(workspace, "static")) 78 | app.Get("/", withAppState(state, home)) 79 | app.Post("/api/sync", withAuthorization(state, sync)) 80 | app.Get("/:id", withAppState(state, note)) 81 | app.Get("/*", withAppState(state, notes)) 82 | 83 | port := os.Getenv("PORT") 84 | if port == "" { 85 | port = "3000" 86 | } 87 | 88 | app.Listen(fmt.Sprintf(":%s", port)) 89 | } 90 | 91 | // loadConfig loads the application configuration from the specified www path 92 | func loadConfig(configPath string) (*config.Config, error) { 93 | var conf config.Config 94 | if err := common.ParseYAMLConfig(configPath, &conf); err != nil { 95 | return nil, err 96 | } 97 | return &conf, nil 98 | } 99 | 100 | // home handles the root path ("/") request 101 | // It either shows the welcome page for empty notes or renders the root note 102 | func home(state *AppState, c *fiber.Ctx) error { 103 | note, err := state.db.GetNote(state.rootNoteId) 104 | if err != nil { 105 | return fiber.ErrInternalServerError 106 | } 107 | 108 | if note == nil { 109 | count, err := state.db.CountNote() 110 | if err != nil { 111 | return c.Render("500", fiber.Map{}) 112 | } 113 | 114 | is_notes_empty := "true" 115 | if count > 0 { 116 | is_notes_empty = "false" 117 | } 118 | 119 | data := templateValues(state, fiber.Map{ 120 | "root_note_name": state.config.RootNoteName, 121 | "is_notes_empty": is_notes_empty, 122 | }) 123 | 124 | return c.Render("welcome", data) 125 | } 126 | 127 | data := templateValues(state, fiber.Map{ 128 | "title": state.config.Name, 129 | "description": state.config.Description, 130 | "notes": []common.Note{*note}, 131 | }) 132 | 133 | return c.Render("home", data) 134 | } 135 | 136 | // note handles single note display requests 137 | // It supports both regular HTTP requests and HTMX requests with proper URL handling 138 | func note(state *AppState, c *fiber.Ctx) error { 139 | noteId := c.Params("id") 140 | fromNoteId := c.Get("X-From-Note-Id") 141 | currentURL := c.Get("HX-Current-URL") 142 | 143 | isHtmxReq := c.Get("HX-Request") == "true" 144 | 145 | if isHtmxReq && (fromNoteId == "" || currentURL == "") { 146 | return fiber.ErrBadRequest 147 | } 148 | 149 | note, err := state.db.GetNote(noteId) 150 | if err != nil { 151 | return fiber.ErrInternalServerError 152 | } 153 | 154 | if note == nil { 155 | return fiber.ErrNotFound 156 | } 157 | 158 | if isHtmxReq { 159 | var pushURL strings.Builder 160 | 161 | parsedURL, err := url.Parse(currentURL) 162 | if err != nil { 163 | return fiber.ErrBadRequest 164 | } 165 | currentPath := parsedURL.Path 166 | 167 | if currentPath == "/" { 168 | pushURL.WriteString(fmt.Sprintf("/%s/%s", state.rootNoteId, noteId)) 169 | } else { 170 | pathParts := strings.Split(currentPath, "/") 171 | for _, part := range pathParts { 172 | if part == "" { 173 | continue 174 | } 175 | pushURL.WriteString("/") 176 | pushURL.WriteString(part) 177 | if part == fromNoteId { 178 | pushURL.WriteString("/") 179 | pushURL.WriteString(noteId) 180 | break 181 | } 182 | } 183 | } 184 | 185 | linkToThis, err := state.db.GetNotesByLinkTo(noteId) 186 | if err != nil { 187 | return fiber.ErrInternalServerError 188 | } 189 | note.LinkToThis = linkToThis 190 | 191 | c.Set("HX-Push-Url", pushURL.String()) 192 | return c.Render("note", fiber.Map{ 193 | "note": note, 194 | }) 195 | } 196 | 197 | note.LoadLinkToThisNotes(state.db) 198 | 199 | data := templateValues(state, fiber.Map{ 200 | "title": note.Title, 201 | "description": note.Description, 202 | "notes": []common.Note{*note}, 203 | }) 204 | 205 | return c.Render("home", data) 206 | } 207 | 208 | // notes handles requests for displaying multiple notes 209 | // It processes the path to extract note IDs and renders them in a combined view 210 | func notes(state *AppState, c *fiber.Ctx) error { 211 | // Deduplication note id 212 | seen := make(map[string]bool) 213 | ids := make([]string, 0) 214 | for _, id := range strings.Split(c.Path(), "/") { 215 | if id != "" && !seen[id] { 216 | seen[id] = true 217 | ids = append(ids, id) 218 | } 219 | } 220 | 221 | notes, err := state.db.GetNotesByIDs(ids) 222 | if err != nil { 223 | return fiber.ErrInternalServerError 224 | } 225 | 226 | // Reorder notes according to ids order 227 | orderedNotes := make([]common.Note, len(ids)) 228 | noteMap := make(map[string]common.Note) 229 | for _, note := range notes { 230 | noteMap[note.ID] = note 231 | } 232 | for i, id := range ids { 233 | if note, ok := noteMap[id]; ok { 234 | orderedNotes[i] = note 235 | } 236 | } 237 | notes = orderedNotes 238 | 239 | for i := range notes { 240 | notes[i].LoadLinkToThisNotes(state.db) 241 | } 242 | 243 | title := fmt.Sprintf("%s - %s", common.Notes(notes).Titles(), state.config.Name) 244 | 245 | data := templateValues(state, fiber.Map{ 246 | "title": title, 247 | "description": title, 248 | "notes": notes, 249 | }) 250 | 251 | return c.Render("home", data) 252 | } 253 | 254 | func templateValues(state *AppState, values fiber.Map) fiber.Map { 255 | res := fiber.Map{ 256 | "config": fiber.Map{ 257 | "lang": state.config.Lang, 258 | "name": state.config.Name, 259 | "description": state.config.Description, 260 | "templates": state.config.Templates, 261 | }, 262 | } 263 | 264 | for key, value := range values { 265 | res[key] = value 266 | } 267 | 268 | return res 269 | } 270 | 271 | // sync handles database synchronization requests 272 | // It accepts a database file upload and updates the server's database 273 | func sync(state *AppState, c *fiber.Ctx) error { 274 | file, err := c.FormFile("db") 275 | if err != nil { 276 | return fiber.ErrBadRequest 277 | } 278 | 279 | uploadedFile, err := file.Open() 280 | if err != nil { 281 | return fiber.ErrInternalServerError 282 | } 283 | defer uploadedFile.Close() 284 | 285 | bytes, err := io.ReadAll(uploadedFile) 286 | if err != nil { 287 | return fiber.ErrInternalServerError 288 | } 289 | 290 | if err := state.db.FromBytes(bytes); err != nil { 291 | return fiber.ErrInternalServerError 292 | } 293 | 294 | return nil 295 | } 296 | 297 | type withStateHandler func(state *AppState, c *fiber.Ctx) error 298 | 299 | // withAppState is a middleware that injects the application state into request handlers 300 | func withAppState(state *AppState, handler withStateHandler) fiber.Handler { 301 | return func(c *fiber.Ctx) error { 302 | return handler(state, c) 303 | } 304 | } 305 | 306 | // withAuthorization is a middleware that validates API key authentication 307 | // It checks for a valid Bearer token in the Authorization header 308 | func withAuthorization(state *AppState, handler withStateHandler) fiber.Handler { 309 | return func(c *fiber.Ctx) error { 310 | apiKey := c.Get("Authorization") 311 | if apiKey == "" { 312 | return fiber.ErrUnauthorized 313 | } 314 | 315 | apiKey = strings.TrimPrefix(apiKey, "Bearer ") 316 | 317 | if apiKey != state.config.APIKey { 318 | return fiber.ErrUnauthorized 319 | } 320 | 321 | return handler(state, c) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /server/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 404 7 | 8 | 9 | 10 | 11 |

404

12 | {% if message %} 13 |

{{ message }}

14 | {% else %} 15 |

Page not found

16 | {% endif %} 17 | 18 | 19 | -------------------------------------------------------------------------------- /server/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 500 - Internal Server Error 7 | 8 | 9 | 10 | 11 |

500 - Internal Server Error

12 | 13 | 14 | -------------------------------------------------------------------------------- /server/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | 11 | 12 |
13 |
16 |
17 |

18 | {{ config.name }} 19 |

20 |
21 |
22 | Powered by MarkdownBrain 23 |
24 |
25 |
26 |
27 | {% for note in notes %} 28 | {% include "note.html" with note=note %} 29 | {% endfor %} 30 |
31 |
32 |
33 | {% if config.HtmxJsUrl %} 34 | 35 | {% else %} 36 | 37 | {% endif %} 38 | {% include "js.html" %} 39 | 40 | {% for template in config.templates %} 41 | {{ template|safe }} 42 | {% endfor %} 43 | 44 | 45 | -------------------------------------------------------------------------------- /server/templates/js.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/templates/note.html: -------------------------------------------------------------------------------- 1 |
5 |
6 |

{{ note.Title }}

7 | {{ note.HTMLContent | safe }} 8 |

Last updated: {{ note.LastUpdatedAt }}.

9 |
10 | 11 | {% if note.LinkToThis %} 12 |
13 |
14 |

Link to this note

15 |
16 | {% for n in note.LinkToThis %} 17 | 30 | {% endfor %} 31 |
32 |
33 |
34 | {% endif %} 35 |
36 | -------------------------------------------------------------------------------- /server/templates/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MarkdownBrain Self Hosted 7 | 8 | 9 | 60 | 61 | 62 |
63 |

MarkdownBrain

64 |
65 |
66 |

Please config your server, and sync your notes.

67 |

Server Details

68 |
69 |

Server:

70 | 71 |
72 |
73 |

Root note name

74 |
75 |
76 |

77 | If your already configured, please ensure your root note name correct. 78 |

79 |

{{ root_note_name }}

80 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /server/www/config.yml: -------------------------------------------------------------------------------- 1 | root_note_name: "Welcome" 2 | lang: "en" 3 | name: "MarkdownBrain" 4 | description: "MarkdownBrain" 5 | api_key: "1234567890" 6 | 7 | # Add custom html 8 | templates: [ 9 | "" 10 | ] 11 | 12 | # Use CDN 13 | # htmx_js_url: "https://cdn.jsdelivr.net/npm/htmx.org@2.0.4/dist/htmx.min.js" 14 | -------------------------------------------------------------------------------- /server/www/data/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackstorm/markdownbrain/c9ce7560c1f81c83c6698912a23f50923d492707/server/www/data/.keep -------------------------------------------------------------------------------- /server/www/static/app.css: -------------------------------------------------------------------------------- 1 | *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"\201C""\201D""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-top:2em;margin-bottom:2em}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-family:inherit;color:var(--tw-prose-kbd);box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows)/10%);font-size:.875em;border-radius:.3125rem;padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;padding-inline-start:.375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:"`"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding-top:.8571429em;padding-inline-end:1.1428571em;padding-bottom:.8571429em;padding-inline-start:1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){width:100%;table-layout:auto;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:17 24 39;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.5714286em;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.static{position:static}.sticky{position:sticky}.top-0{top:0}.mb-2{margin-bottom:.5rem}.mt-4{margin-top:1rem}.block{display:block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-screen{height:100vh}.w-full{width:100%}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.divide-x>:not([hidden])~:not([hidden]){--tw-divide-x-reverse:0;border-right-width:calc(1px*var(--tw-divide-x-reverse));border-left-width:calc(1px*(1 - var(--tw-divide-x-reverse)))}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-hidden{overflow-y:hidden}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity,1))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.p-2{padding:.5rem}.p-4{padding:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}body{background-color:#fafafc}@keyframes fadeInScale{0%{opacity:0;transform:scale(.98)}to{opacity:1;transform:scale(1)}}.note{animation:fadeInScale .1s ease-out}.note-link{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity,1))}.note-link:hover{text-decoration-line:underline}.last-updated-at{font-size:.75rem;line-height:1rem;--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.hover\:text-blue-700:hover{--tw-text-opacity:1;color:rgb(29 78 216/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}@media (min-width:768px){.md\:mt-8{margin-top:2rem}.md\:w-\[625px\]{width:625px}.md\:max-w-\[625px\]{max-width:625px}.md\:p-8{padding:2rem}.md\:px-8{padding-left:2rem;padding-right:2rem}} -------------------------------------------------------------------------------- /server/www/static/app.js: -------------------------------------------------------------------------------- 1 | function highlightNoteById(id) { 2 | const note = document.getElementById(id); 3 | note.style.transition = "background-color 0.3s ease, transform 0.3s ease"; 4 | note.style.backgroundColor = "lightyellow"; 5 | note.style.transform = "scale(1.01)"; 6 | setTimeout(() => { 7 | note.style.backgroundColor = ""; 8 | note.style.transform = "scale(1)"; 9 | }, 500); 10 | 11 | return note 12 | } 13 | 14 | htmx.on("htmx:beforeSwap", (evt) => { 15 | const target = evt.target; 16 | if ( 17 | target.className.includes("note") && 18 | target.id != undefined && 19 | target.id.startsWith("note") 20 | ) { 21 | const targetElement = document.getElementById(target.id); 22 | if (targetElement) { 23 | let nextSibling = targetElement.nextElementSibling; 24 | while (nextSibling) { 25 | const elementToRemove = nextSibling; 26 | nextSibling = nextSibling.nextElementSibling; 27 | elementToRemove.parentNode.removeChild(elementToRemove); 28 | } 29 | } 30 | } 31 | }); 32 | 33 | htmx.on("htmx:afterSwap", (evt) => { 34 | const notes = document.querySelectorAll(".note"); 35 | const titles = []; 36 | notes.forEach(note => { 37 | const h1 = note.querySelector('article h1'); 38 | if (h1) { 39 | titles.push(h1.textContent); 40 | } 41 | }); 42 | document.title = titles.join('|'); 43 | }); 44 | 45 | htmx.on("htmx:beforeRequest", (evt) => { 46 | const target = evt.target; 47 | // /example-id -> example-id 48 | const href = target.getAttribute("href").split('/')[1]; 49 | const pathSegments = window.location.pathname.split('/'); 50 | for (let path of pathSegments) { 51 | if (path != "" && path == href) { 52 | evt.preventDefault(); 53 | highlightNoteById(`note-${href}`); 54 | } 55 | } 56 | }); 57 | 58 | htmx.on("htmx:afterSwap", (evt) => { 59 | noteWindowSizeAdjust(); 60 | }); 61 | -------------------------------------------------------------------------------- /server/www/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackstorm/markdownbrain/c9ce7560c1f81c83c6698912a23f50923d492707/server/www/static/favicon.ico -------------------------------------------------------------------------------- /server/www/static/htmx.min.js: -------------------------------------------------------------------------------- 1 | var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.3"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=de;Q.ajax=Rn;Q.find=r;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=h;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:dn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:i,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:dt,triggerEvent:de,triggerErrorEvent:fe,withExtensions:Ft};const o=["get","post","put","delete","patch"];const R=o.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function h(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function i(e,t){while(e&&!t(e)){e=c(e)}return e||null}function H(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;i(t,function(e){return!!(r=H(t,ue(e),n))});if(r!=="unset"){return r}}function d(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function N(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function A(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(A(e)){const t=N(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){C(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e=0}function le(e){const t=e.getRootNode&&e.getRootNode();if(t&&t instanceof window.ShadowRoot){return ne().body.contains(t.host)}else{return ne().body.contains(e)}}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){C(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function r(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return r(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||d(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(e,t,n){e=y(e);if(t.indexOf("closest ")===0){return[g(ue(e),ge(t.substr(8)))]}else if(t.indexOf("find ")===0){return[r(f(e),ge(t.substr(5)))]}else if(t==="next"){return[ue(e).nextElementSibling]}else if(t.indexOf("next ")===0){return[pe(e,ge(t.substr(5)),!!n)]}else if(t==="previous"){return[ue(e).previousElementSibling]}else if(t.indexOf("previous ")===0){return[me(e,ge(t.substr(9)),!!n)]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else if(t==="root"){return[m(e,!!n)]}else if(t==="host"){return[e.getRootNode().host]}else if(t.indexOf("global ")===0){return p(e,t.slice(7),true)}else{return M(f(m(e,!!n)).querySelectorAll(ge(t)))}}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return r(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){C('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(i(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substr(0,e.indexOf(":"));n=e.substr(e.indexOf(":")+1,e.length)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!de(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){de(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=r("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=r("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=r("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","
");e=r("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ne(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ae(f(e));de(e,"htmx:load")}}function Ae(e){const t="[autofocus]";const n=$(d(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function u(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ne(o))}}}function Ie(e,t){let n=0;while(n0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}de(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function w(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=w(e,Qe).trim();e.shift()}else{t=w(e,b)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{w(o,v);const l=o.length;const c=w(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};w(o,v);u.pollInterval=h(w(o,/[,\[\s]/));w(o,v);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}w(o,v);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=h(w(o,b))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=w(o,b);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const d=rt(o);if(d.length>0){s+=" "+d}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=h(w(o,b))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=w(o,b)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=w(o,b)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}w(o,v)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}w(o,v)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(d(e,"form")){return[{trigger:"submit"}]}else if(d(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(d(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){a(n);return}he(r,o,n,t)},n,e,true)})}}function dt(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(d(n,'input[type="submit"], button')&&g(n,"form")!==null){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ht(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(ht(l,e)){return}if(a||dt(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!d(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){de(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){de(l,"htmx:trigger");c(l,e)},u.delay)}else{de(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){de(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){de(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(o,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){a(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function Nt(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function At(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){de(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;de(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;de(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){de(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});de(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!d(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:Nn(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function dn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.substr(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function hn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!hn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.substr(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const d=l.substr("focus-scroll:".length);r.focusScroll=d=="true"}else if(e==0){r.swapStyle=l}else{C("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||d(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.substr(11);t=true}else if(e.indexOf("js:")===0){e=e.substr(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||!e&&!y(r.source)){e=ve}return he(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return de(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function Nn(r){return new Proxy(r,{get:function(e,t){if(typeof t==="symbol"){return Reflect.get(e,t)}if(t==="toJSON"){return()=>Object.fromEntries(r)}if(t in e){if(typeof e[t]==="function"){return function(){return r[t].apply(r,arguments)}}else{return e[t]}}const n=r.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const N=ee(a,"formmethod");if(N!=null){if(N.toLowerCase()!=="dialog"){t=N}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(de(r,"htmx:confirm",G)===false){oe(s);return e}}let d=r;let h=re(r,"hx-sync");let g=null;let F=false;if(h){const A=h.split(":");const I=A[0].trim();if(I==="this"){d=Se(r,"hx-sync")}else{d=ue(ae(r,I))}h=(A[1]||"drop").trim();u=ie(d);if(h==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(h==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(h==="replace"){de(d,"htmx:abort")}else if(h.indexOf("queue")===0){const W=h.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){de(d,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!de(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=dn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:Nn(w),unfilteredFormData:v,unfilteredParameters:Nn(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!de(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){de(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}de(r,"htmx:afterRequest",H);de(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){de(e,"htmx:afterRequest",H);de(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!de(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){de(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});de(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function An(e,t){const n=t.xhr;let r=null;let o=null;if(O(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(O(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(O(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){de(e,"htmx:restored",{document:ne(),triggerEvent:de})})}else{if(n){n(e)}}};E().setTimeout(function(){de(e,"htmx:load",{});e=null},0)});return Q}(); -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./server/**/*.{html,js,go}", "./cli/**/*.{html,js,go}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("@tailwindcss/typography")], 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | background-color: #fafafc; 7 | } 8 | 9 | @keyframes fadeInScale { 10 | 0% { 11 | opacity: 0; 12 | transform: scale(0.98); 13 | } 14 | 15 | 100% { 16 | opacity: 1; 17 | transform: scale(1); 18 | } 19 | } 20 | 21 | .note { 22 | animation: fadeInScale 0.1s ease-out; 23 | } 24 | 25 | .note-link { 26 | @apply text-blue-600 hover:underline; 27 | } 28 | 29 | .last-updated-at { 30 | @apply text-xs text-zinc-400; 31 | } 32 | --------------------------------------------------------------------------------