9 |
10 |
11 | # Documentation
12 |
13 | * [Documentation](https://xlog.emadelsaid.com/)
14 | * [Installation](https://xlog.emadelsaid.com/docs/Installation/)
15 | * [Usage](https://xlog.emadelsaid.com/docs/Usage/)
16 | * [Generating static site](https://xlog.emadelsaid.com/tutorials/Creating%20a%20site)
17 | * [Overriding Assets](https://xlog.emadelsaid.com/docs/Assets)
18 | * [Extensions](https://xlog.emadelsaid.com/docs/extensions/)
19 | * [Writing Your Own Extension](https://xlog.emadelsaid.com/tutorials/Hello%20world%20extension/)
20 |
21 | # License
22 |
23 | Xlog is released under [MIT license](LICENSE)
24 |
--------------------------------------------------------------------------------
/cmd/assets/dubai-font.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Dubai W23';
3 | font-style: normal;
4 | font-weight: bold;
5 | src: local('Dubai W23 Bold Regular'), url('/dubai-font/DubaiW23-Bold.woff') format('woff');
6 | unicode-range: U+0600-06FF;
7 | }
8 |
9 |
10 | @font-face {
11 | font-family: 'Dubai W23';
12 | font-style: normal;
13 | font-weight: regular;
14 | src: local('Dubai W23 Regular Regular'), url('/dubai-font/DubaiW23-Regular.woff') format('woff');
15 | unicode-range: U+0600-06FF;
16 | }
17 |
--------------------------------------------------------------------------------
/cmd/assets/dubai-font/DubaiW23-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emad-elsaid/xlog/00ddaff0ffbc9a3e26326e0564a6f66fd1383d65/cmd/assets/dubai-font/DubaiW23-Bold.woff
--------------------------------------------------------------------------------
/cmd/assets/dubai-font/DubaiW23-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emad-elsaid/xlog/00ddaff0ffbc9a3e26326e0564a6f66fd1383d65/cmd/assets/dubai-font/DubaiW23-Regular.woff
--------------------------------------------------------------------------------
/cmd/assets/index.js:
--------------------------------------------------------------------------------
1 | require('./styles.scss');
2 |
--------------------------------------------------------------------------------
/cmd/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xlog",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "repository": "https://github.com/emad-elsaid/xlog.git",
6 | "author": "Emad Elsaid ",
7 | "license": "MIT",
8 | "scripts": {
9 | "build": "webpack --mode production",
10 | "watch": "webpack --watch --mode production"
11 | },
12 | "dependencies": {
13 | "@fontsource/inter": "^4.5.14",
14 | "@fortawesome/fontawesome-free": "^6.2.1",
15 | "bulma": "^1.0.0",
16 | "css-loader": "^6.7.3",
17 | "extract-text-webpack-plugin": "^4.0.0-beta.0",
18 | "file-loader": "^6.2.0",
19 | "mini-css-extract-plugin": "^2.7.2",
20 | "resolve-url-loader": "^5.0.0",
21 | "sass-loader": "^13.2.0",
22 | "style-loader": "^3.3.1",
23 | "webpack": "^5.94.0",
24 | "webpack-cli": "^5.0.1"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/cmd/assets/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
3 |
4 | module.exports = {
5 | entry: './index.js',
6 | output: {
7 | path: path.resolve(__dirname, '../../public'),
8 | filename: 'ignored.js',
9 | assetModuleFilename: 'assets/[name][ext][query]'
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
15 | type: 'asset/resource',
16 | },
17 | {
18 | test: /\.s?css$/,
19 | use: [
20 | MiniCssExtractPlugin.loader,
21 | {
22 | loader: 'css-loader'
23 | },
24 | {
25 | loader: 'resolve-url-loader',
26 | },
27 | {
28 | loader: 'sass-loader',
29 | options: {
30 | sourceMap: true
31 | }
32 | }
33 | ]
34 | }]
35 | },
36 | plugins: [
37 | new MiniCssExtractPlugin({
38 | filename: 'style.css'
39 | }),
40 | ]
41 | };
42 |
--------------------------------------------------------------------------------
/cmd/xlog/xlog.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | // Core
5 | "context"
6 |
7 | "github.com/emad-elsaid/xlog"
8 |
9 | // All official extensions
10 | _ "github.com/emad-elsaid/xlog/extensions/all"
11 | )
12 |
13 | func main() {
14 | xlog.Start(context.Background())
15 | }
16 |
--------------------------------------------------------------------------------
/command.go:
--------------------------------------------------------------------------------
1 | package xlog
2 |
3 | import "html/template"
4 |
5 | // Command defines a structure used for 3 categories of lists:
6 | // 1. Commands for Ctrl+K actions menu
7 | // 2. Quick commands displayed in the default template at the top right of the page
8 | // 3. Links displayed in the navigation bar
9 | // The template decides where and how to display commands. it can choose to use them in a different way than the default template
10 | type Command interface {
11 | // Icon returns the Fontawesome icon class name for the Command
12 | Icon() string
13 | // Name of the command. to be displayed in the list
14 | Name() string
15 | // Attrs a map of attributes to their values
16 | Attrs() map[template.HTMLAttr]any
17 | }
18 |
19 | var commands = []func(Page) []Command{}
20 |
21 | // RegisterCommand registers a new command
22 | func RegisterCommand(c func(Page) []Command) {
23 | commands = append(commands, c)
24 | }
25 |
26 | // Commands return the list of commands for a page. when a page is displayed it
27 | // executes all functions registered with RegisterCommand and collect all
28 | // results in one slice. result can be passed to the view to render the commands
29 | // list
30 | func Commands(p Page) []Command {
31 | cmds := []Command{}
32 | for c := range commands {
33 | cmds = append(cmds, commands[c](p)...)
34 | }
35 |
36 | return cmds
37 | }
38 |
39 | var quickCommands = []func(Page) []Command{}
40 |
41 | func RegisterQuickCommand(c func(Page) []Command) {
42 | quickCommands = append(quickCommands, c)
43 | }
44 |
45 | // QuickCommands return the list of QuickCommands for a page. it executes all functions
46 | // registered with RegisterQuickCommand and collect all results in one slice. result
47 | // can be passed to the view to render the Quick commands list
48 | func QuickCommands(p Page) []Command {
49 | cmds := []Command{}
50 | for c := range quickCommands {
51 | cmds = append(cmds, quickCommands[c](p)...)
52 | }
53 |
54 | return cmds
55 | }
56 |
57 | var links = []func(Page) []Command{}
58 |
59 | // Register a new links function, should return a list of Links
60 | func RegisterLink(l func(Page) []Command) {
61 | links = append(links, l)
62 | }
63 |
64 | // Links returns a list of links for a Page. it executes all functions
65 | // registered with RegisterLink and collect them in one slice. Can be passed to
66 | // the view to render in the footer for example.
67 | func Links(p Page) []Command {
68 | lnks := []Command{}
69 | for l := range links {
70 | lnks = append(lnks, links[l](p)...)
71 | }
72 | return lnks
73 | }
74 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3"
2 |
3 | services:
4 | xlog:
5 | build: .
6 | ports:
7 | - "3000:3000"
8 | volumes:
9 | - ~/.xlog:/files
10 |
--------------------------------------------------------------------------------
/docs/.cache/1316c89f817afdf85038ad3a02698a3a4757263c7e28f0d83c34c387d9d5088a.json:
--------------------------------------------------------------------------------
1 | {"URL":"https://www.emadelsaid.com/Why I became a software developer/","Title":" Why I became a software developer | Emad Elsaid","Description":"Why I became a software developer","Image":"https://www.emadelsaid.com/public/IMG_20200217_161802.jpg"}
--------------------------------------------------------------------------------
/docs/.cache/66f5fcbba4e9ef12e70b5b685c478e62f2fe659fbc9438ca7fd7582d22342d0e.json:
--------------------------------------------------------------------------------
1 | {"URL":"https://github.com/yuin/goldmark","Title":"GitHub - yuin/goldmark: :trophy: A markdown parser written in Go. Easy to extend, standard(CommonMark) compliant, well structured.","Description":":trophy: A markdown parser written in Go. Easy to extend, standard(CommonMark) compliant, well structured. - yuin/goldmark","Image":"https://opengraph.githubassets.com/50b0f5b6e0281db8eff566c28a58c4fe74cd5a740e36d037711f44e57e98b8e9/yuin/goldmark"}
--------------------------------------------------------------------------------
/docs/.cache/718ff3b88f310e3dfadb357db10a1d6a2a973217656adbf35b4be9075e57fd59.json:
--------------------------------------------------------------------------------
1 | {"URL":"https://github.com/emad-elsaid/xlog","Title":"GitHub - emad-elsaid/xlog: 💥 Personal knowledge management application. One binary HTTP server. works in any Markdown directory. autolinks pages, hashtags, auto preview images link, screenshare, screenshot, camera recording and audio recording embedded in the note. and fast search through the KB","Description":"💥 Personal knowledge management application. One binary HTTP server. works in any Markdown directory. autolinks pages, hashtags, auto preview images link, screenshare, screenshot, camera recording ...","Image":"https://repository-images.githubusercontent.com/16852661/85412dd7-4794-400d-b0f5-2557ab658078"}
--------------------------------------------------------------------------------
/docs/.cache/9bfa931273abab8cd69e213e5657059e8107cc359d011f3893433a1911b59a71.json:
--------------------------------------------------------------------------------
1 | {"URL":"https://codemirror.net/","Title":"CodeMirror","Description":"In-browser code editor","Image":"http://codemirror.net/style/logo.svg"}
--------------------------------------------------------------------------------
/docs/.cache/dadbd4f3a88b7ac925c9afb369104ff8b429ed32fe02ec2725b94c789610585a.json:
--------------------------------------------------------------------------------
1 | {"URL":"https://commonmark.org/","Title":"CommonMark","Description":"","Image":""}
--------------------------------------------------------------------------------
/docs/.cache/de4b0df00d5d7b3c5e9159bfae73447229522578195227cbee2f21c0f8ea461f.json:
--------------------------------------------------------------------------------
1 | {"URL":"https://bulma.io/","Title":"https://bulma.io/","Description":"Bulma is a free, open source CSS framework based on Flexbox and built with Sass. It's 100% responsive, fully modular, and available for free.","Image":"https://bulma.io/assets/images/bulma-banner.png"}
--------------------------------------------------------------------------------
/docs/404.md:
--------------------------------------------------------------------------------
1 | This usually means that you've typed or followed a bad URL. Occasionally, this can happen if the page has been moved to a new URL (though we try to avoid doing that).
2 |
3 | See [the Wikipedia page on HTTP 404 errors](https://en.wikipedia.org/wiki/HTTP_404) to learn more about HTTP 404 errors in general.
4 |
--------------------------------------------------------------------------------
/docs/API.md:
--------------------------------------------------------------------------------
1 | Xlog is a Go package that has its public API documented and published on https://pkg.go.dev as all Go packages do. Latest documentation can be found here https://pkg.go.dev/github.com/emad-elsaid/xlog
--------------------------------------------------------------------------------
/docs/Assets.md:
--------------------------------------------------------------------------------
1 | Xlog serves any files under current directory with exception of markdown files being accessed without `.md` extension and converted to HTML.
2 |
3 | Besides that it serves files from embded files in the program from the core package or extensions.
4 |
5 | # Overriding asset files
6 |
7 | Embded files are the last resort when looking up a file so to override an asset file you just need to put it in the same path in the current directory. that's all. that simple.
8 |
9 | ## CSS
10 |
11 | - Xlog used to have a Go script to compile CSS/SASS to `public/style.css`.
12 | - That changed to depend on Webpack in this commit 38c8171
13 | - So chdir to `cmd/assets` and either build with `yarn build` or watch changes `yarn watch`
14 |
--------------------------------------------------------------------------------
/docs/Bulma.md:
--------------------------------------------------------------------------------
1 | A CSS only framework. used to provide a basic look for xlog.
2 |
3 | https://bulma.io/
4 |
5 | it's embeded in the binary executable at compile time using Go `embed` package.
--------------------------------------------------------------------------------
/docs/Dependencies.md:
--------------------------------------------------------------------------------
1 | Compile time dependencies:
2 |
3 | * Go compiler
4 | * Goldmark markdown parser
5 |
6 | Run time dependencies:
7 |
8 | * Bulma CSS framework
9 |
--------------------------------------------------------------------------------
/docs/Github.md:
--------------------------------------------------------------------------------
1 | Code is hosted on Github repository.
2 |
3 |
4 | https://github.com/emad-elsaid/xlog
5 |
6 | # Open pull requests
7 |
8 | /github-search-issues repo:emad-elsaid/xlog is:pull-request is:open
--------------------------------------------------------------------------------
/docs/Go package.md:
--------------------------------------------------------------------------------
1 | Xlog is distributed as Go package that has a CLI that uses the package and all `xlog/extensions`. documentation of the Go package API can be found on https://pkg.go.dev/github.com/emad-elsaid/xlog
--------------------------------------------------------------------------------
/docs/Installation.md:
--------------------------------------------------------------------------------
1 | # Download latest binary
2 |
3 | Github has a release for each xlog version tag. it has binaries built for (Windows, Linux, MacOS) for several architectures. you can download the latest version from this page: https://github.com/emad-elsaid/xlog/releases/latest
4 |
5 | # Using Go
6 |
7 | ```bash
8 | go install github.com/emad-elsaid/xlog/cmd/xlog@latest
9 | ```
10 |
11 | # From source
12 |
13 | ```bash
14 | git clone git@github.com:emad-elsaid/xlog.git
15 | cd xlog
16 | go run ./cmd/xlog # to run it
17 | go install ./cmd/xlog # to install it to Go bin.
18 | ```
19 |
20 | # Arch Linux (AUR)
21 |
22 | * Xlog is published to AUR: https://aur.archlinux.org/packages/xlog-git
23 | * Using `yay` for example:
24 |
25 | ```bash
26 | yay -S xlog-git
27 | ```
28 |
29 | # From source with docker-compose
30 |
31 | ```bash
32 | git clone git@github.com:emad-elsaid/xlog.git
33 | cd xlog
34 | docker-composer build
35 | docker-composer run
36 | ```
37 |
38 | ```info
39 | Xlog container attach `~/.xlog` as a volume and will write pages to it.
40 | ```
41 |
42 | # Docker
43 |
44 | Releases are packaged as docker images and pushed to GitHub
45 |
46 | ```bash
47 | docker pull ghcr.io/emad-elsaid/xlog:latest
48 | docker run -p 3000:3000 -v ~/.xlog:/files ghcr.io/emad-elsaid/xlog:latest
49 | ```
--------------------------------------------------------------------------------
/docs/Readonly.md:
--------------------------------------------------------------------------------
1 | Xlog works in 2 modes:
2 |
3 | * Read/Write
4 | * ReadOnly
5 |
6 | By default xlog server works in Read/Write mode where you can edit and delete files. this mode is not for production use. it's meant for local personal use. and this is meant for the first usecase: taking personal notes, local digital gardening.
7 |
8 | ReadOnly mode which can be specified using `--readonly=true` flag. This flag is checked by xlog and extensions to turn of any code that writes to the filesystem.
9 |
10 | /alert don't run xlog server on production server neither in read/write nor in readonly. as it's meant for personal local use.
11 |
12 | Generate static website process will turn on readonly mode automatically.
13 |
14 | Any extensions that writes or modify the filesystem is responsible for checking if `Config.Readonly` global variable is true and make sure that part is not executed.
15 |
--------------------------------------------------------------------------------
/docs/Security.md:
--------------------------------------------------------------------------------
1 | Xlog is designed to be accessed by trusted clients inside trusted environments. This means that usually it is not a good idea to expose the Xlog instance directly to the internet or, in general, to an environment where untrusted clients can directly access the Xlog TCP port.
2 |
3 | If you want to expose it over unsecure HTTP (for development purposes or in LAN), please use `--serve-insecure true` flag.
4 |
5 | # Listening on specific network interface
6 |
7 | Xlog accepts `--bind` flag that defines the interface which xlog should listen to. `--bind` is in the format `:`.
8 |
9 | - To listen on all interfaces on port 3000 pass `--bind 0.0.0.0:3000`
10 | - To listen on specific interface pass the interface IP address `--bind 192.168.8.200:3000`
11 |
12 | # Readonly mode
13 |
14 | Xlog accept a `--readonly` flag to signal all features not to write to the disk. Readonly mode is not a safe measure for exposing the server to the internet. additionally make sure you sandbox the process in a restricted environment such as docker, CGROUPS or another user that has readonly access to the disk.
15 |
16 | Extensions can ignore the readonly flag so make sure you use trusted extensions only in case you intend to expose xlog to the internet.
17 |
18 |
19 | # Reporting Security Issues
20 |
21 | Please report any issues to me on Keybase: https://keybase.io/emadelsaid
22 |
--------------------------------------------------------------------------------
/docs/Upgrading.md:
--------------------------------------------------------------------------------
1 | # Upgrading to V2
2 |
3 | If you were a user for v1 and would like to upgrade to v2 please take the following steps:
4 |
5 | * `--sidebar` command-line argument is removed. it had no effect for a long time already and was kept for backward compatibility
6 | * The `book` extension is renamed to `blocks` and has the same `book` shortcode. if you're importing it manually you need to change the import path to import `blocks` instead
7 | * For extensions development
8 | * You're now required to `xlog.RegisterExtension` your extension in your `init()` function then `Register*` the rest of your components in the extension `.Init()` function instead of the global one. this allow for future development to enable/disable extensions by the user.
9 | * Your extension can now check for `xlog.Config.Readonly` instead of `xlog.READONLY` during initialization (extension `Init()`)
10 | * `Get/Post/Delete/..etc` doesn't accept middlewares parameters anymore
11 | * The version extension has been removed as it was incomplete. if you are running xlog on the same directory that has `.versions` subdirectories you can remove them by running `rm -rf *.versions`
12 | * `github.repo` and `github.branch` are removed in favor of `github.url` which is the full URL of the editing. so it should work with other git online editors
13 | * `custom_css` was removed as its functionality can be achieved by using `custom.head`
14 | * `custom_head/before_view/after_view` name changed to `custom.` replacing the `_` with `.` for consistency with other flags
15 |
--------------------------------------------------------------------------------
/docs/digital gardening.md:
--------------------------------------------------------------------------------
1 | A way for taking notes or writing down knowledge. it depends on rough and incomplete posts/notes instead of complete full blog posts.
2 |
3 | You end up with personal wiki. non-hierarchical and not complete. but it represent the scattered and linked knowledge of a person.
4 |
5 | This is Xlog digital garden. a set of markdown files that contains the knowledge related to xlog. and built with it into static website. everytime a page name is mentioned in text xlog converts it to a link. so you get to navigate the text and expand on abbreviations or concepts as you go.
6 |
7 |
8 | # Resources
9 | https://maggieappleton.com/garden-history/
10 |
11 |
--------------------------------------------------------------------------------
/docs/extensions.md:
--------------------------------------------------------------------------------
1 | Xlog is built to be small core that offers small set of features. And focus on offering a developer friendly public API to allow extending it with more features.
2 |
3 | # Extension points
4 |
5 | - Add any HTTP route with your handler function
6 | - Add Goldmark extension for parsing or rendering
7 | - Add a Preprocessor function to process page content before converting to HTML
8 | - Listen to Page events such as Write or Delete.
9 | - Define a helper function for templates
10 | - Add a directory to be parsed as a template
11 | - Add widgets in selected areas of the view page such as before or after rendered HTML
12 | - Add a command to the list of commands triggered with `Ctrl+K` which can execute arbitrary Javascript.
13 | - Add a route to be exported in case of building static site
14 | - Add arbitrary link to pages or any URL
15 | - Add quick command to appear on top of the view page
16 |
17 | # Overview
18 |
19 | An extension is a
20 |
21 | * Go module/package that imports xlog package
22 | * Can be hosted anywhere
23 | * Implements `xlog.Extension` interface
24 | * Has an `Init` function to register all of its components using `Register*`
25 | * Uses `RegisterExtension` functions in the `init` function of the package to register the extension
26 | * Adds or improves a feature in xlog using one or more of the extension points.
27 | * Imported by a the `main` package of your knowledgebase along with all other extensions and Xlog itself. an example can be found in Xlog CLI
28 |
29 | # Creating extensions
30 |
31 | * Hello world extension
32 |
--------------------------------------------------------------------------------
/docs/goldmark.md:
--------------------------------------------------------------------------------
1 | A markdown parser written in Go. Used in Xlog for its extensibility. extensions can easily add feature to its parser and HTML renderer.
2 |
3 | https://github.com/yuin/goldmark
--------------------------------------------------------------------------------
/docs/helper.md:
--------------------------------------------------------------------------------
1 | A helper function is a function that is used in an html template. the output of the function gets printed to the template. a helper function can take one or more parameters.
2 |
3 | Helper functions are Go `html/template` concept it's not introduced by xlog. an example can be found in html/template [documentation](https://pkg.go.dev/html/template#example-Template-Helpers).
4 |
5 | Extensions can define their own helpers to be used by any template using [`RegisterHelper`](https://pkg.go.dev/github.com/emad-elsaid/xlog#RegisterHelper) function. Registering a new helper has to be in the extension `init` function to be find at the time of parsing the templates.
--------------------------------------------------------------------------------
/docs/public/custom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emad-elsaid/xlog/00ddaff0ffbc9a3e26326e0564a6f66fd1383d65/docs/public/custom.png
--------------------------------------------------------------------------------
/docs/public/d32ac848ea161f9b384ed2ed81d657e3f150bcd3aa355a75741b95c76b873898.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emad-elsaid/xlog/00ddaff0ffbc9a3e26326e0564a6f66fd1383d65/docs/public/d32ac848ea161f9b384ed2ed81d657e3f150bcd3aa355a75741b95c76b873898.avif
--------------------------------------------------------------------------------
/docs/public/puzzle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emad-elsaid/xlog/00ddaff0ffbc9a3e26326e0564a6f66fd1383d65/docs/public/puzzle.png
--------------------------------------------------------------------------------
/docs/public/shortcode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emad-elsaid/xlog/00ddaff0ffbc9a3e26326e0564a6f66fd1383d65/docs/public/shortcode.png
--------------------------------------------------------------------------------
/docs/public/sprout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emad-elsaid/xlog/00ddaff0ffbc9a3e26326e0564a6f66fd1383d65/docs/public/sprout.png
--------------------------------------------------------------------------------
/docs/public/static.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emad-elsaid/xlog/00ddaff0ffbc9a3e26326e0564a6f66fd1383d65/docs/public/static.png
--------------------------------------------------------------------------------
/docs/public/website.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emad-elsaid/xlog/00ddaff0ffbc9a3e26326e0564a6f66fd1383d65/docs/public/website.png
--------------------------------------------------------------------------------
/each_test.go:
--------------------------------------------------------------------------------
1 | package xlog
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestIsIgnoredPath(t *testing.T) {
10 | assert.True(t, IsIgnoredPath(".git/config"))
11 | assert.True(t, IsIgnoredPath(".versions/config"))
12 | assert.False(t, IsIgnoredPath("index.md"))
13 | assert.False(t, IsIgnoredPath("something/something"))
14 | }
15 |
16 | func TestIsNil(t *testing.T) {
17 | assert.True(t, isNil[Page](nil))
18 | assert.True(t, isNil[*Page](nil))
19 | }
20 |
--------------------------------------------------------------------------------
/events.go:
--------------------------------------------------------------------------------
1 | package xlog
2 |
3 | import "log/slog"
4 |
5 | type (
6 | // a type used to define events to be used when the page is manipulated for
7 | // example modified, renamed, deleted...etc.
8 | PageEvent int
9 | // a function that handles a page event. this should be implemented by an
10 | // extension and then registered. it will get executed when the event is
11 | // triggered
12 | PageEventHandler func(Page) error
13 | )
14 |
15 | // List of page events. extensions can use these events to register a function
16 | // to be executed when this event is triggered. extensions that require to be
17 | // notified when the page is created or overwritten or deleted should register
18 | // an event handler for the interesting events.
19 | const (
20 | PageChanged PageEvent = iota
21 | PageDeleted
22 | PageNotFound // user requested a page that's not found
23 | )
24 |
25 | // a map to keep all page events and respective list of event handlers
26 | var pageEvents = map[PageEvent][]PageEventHandler{}
27 |
28 | // Register an event handler to be executed when PageEvent is triggered.
29 | // extensions can use this to register hooks under specific page events.
30 | // extensions that keeps a cached version of the pages list for example needs to
31 | // register handlers to update its cache
32 | func Listen(e PageEvent, h PageEventHandler) {
33 | if _, ok := pageEvents[e]; !ok {
34 | pageEvents[e] = []PageEventHandler{}
35 | }
36 |
37 | pageEvents[e] = append(pageEvents[e], h)
38 | }
39 |
40 | // Trigger event handlers for a specific page event. page methods use this
41 | // function to trigger all registered handlers when the page is edited or
42 | // deleted for example.
43 | func Trigger(e PageEvent, p Page) {
44 | if _, ok := pageEvents[e]; !ok {
45 | return
46 | }
47 |
48 | for _, h := range pageEvents[e] {
49 | if err := h(p); err != nil {
50 | slog.Error("Failed to execute handler for event", "event", e, "handler", h, "error", err)
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/extensions.go:
--------------------------------------------------------------------------------
1 | package xlog
2 |
3 | import (
4 | "log/slog"
5 | "slices"
6 | "strings"
7 | )
8 |
9 | type Extension interface {
10 | Name() string
11 | Init()
12 | }
13 |
14 | var extensions = []Extension{}
15 |
16 | func RegisterExtension(e Extension) {
17 | extensions = append(extensions, e)
18 | }
19 |
20 | func initExtensions() {
21 | if Config.DisabledExtensions == "all" {
22 | slog.Info("extensions", "disabled", "all")
23 | return
24 | }
25 |
26 | disabled := strings.Split(Config.DisabledExtensions, ",")
27 | disabledNames := []string{} // because the user can input wrong extension name
28 | enabledNames := []string{}
29 | for i := range extensions {
30 | if slices.Contains(disabled, extensions[i].Name()) {
31 | disabledNames = append(disabledNames, extensions[i].Name())
32 | continue
33 | }
34 |
35 | extensions[i].Init()
36 | enabledNames = append(enabledNames, extensions[i].Name())
37 | }
38 |
39 | slog.Info("extensions", "enabled", enabledNames, "disabled", disabled)
40 | }
41 |
--------------------------------------------------------------------------------
/extensions/all/all.go:
--------------------------------------------------------------------------------
1 | package all
2 |
3 | import (
4 | _ "github.com/emad-elsaid/xlog/extensions/activitypub"
5 | _ "github.com/emad-elsaid/xlog/extensions/autolink"
6 | _ "github.com/emad-elsaid/xlog/extensions/autolink_pages"
7 | _ "github.com/emad-elsaid/xlog/extensions/blocks"
8 | _ "github.com/emad-elsaid/xlog/extensions/custom_widget"
9 | _ "github.com/emad-elsaid/xlog/extensions/date"
10 | _ "github.com/emad-elsaid/xlog/extensions/disqus"
11 | _ "github.com/emad-elsaid/xlog/extensions/editor"
12 | _ "github.com/emad-elsaid/xlog/extensions/embed"
13 | _ "github.com/emad-elsaid/xlog/extensions/file_operations"
14 | _ "github.com/emad-elsaid/xlog/extensions/frontmatter"
15 | _ "github.com/emad-elsaid/xlog/extensions/github"
16 | _ "github.com/emad-elsaid/xlog/extensions/gpg"
17 | _ "github.com/emad-elsaid/xlog/extensions/hashtags"
18 | _ "github.com/emad-elsaid/xlog/extensions/heading"
19 | _ "github.com/emad-elsaid/xlog/extensions/hotreload"
20 | _ "github.com/emad-elsaid/xlog/extensions/html"
21 | _ "github.com/emad-elsaid/xlog/extensions/images"
22 | _ "github.com/emad-elsaid/xlog/extensions/link_preview"
23 | _ "github.com/emad-elsaid/xlog/extensions/manifest"
24 | _ "github.com/emad-elsaid/xlog/extensions/mathjax"
25 | _ "github.com/emad-elsaid/xlog/extensions/mermaid"
26 | _ "github.com/emad-elsaid/xlog/extensions/opengraph"
27 | _ "github.com/emad-elsaid/xlog/extensions/pandoc"
28 | _ "github.com/emad-elsaid/xlog/extensions/photos"
29 | _ "github.com/emad-elsaid/xlog/extensions/recent"
30 | _ "github.com/emad-elsaid/xlog/extensions/rss"
31 | _ "github.com/emad-elsaid/xlog/extensions/rtl"
32 | _ "github.com/emad-elsaid/xlog/extensions/search"
33 | _ "github.com/emad-elsaid/xlog/extensions/shortcode"
34 | _ "github.com/emad-elsaid/xlog/extensions/sitemap"
35 | _ "github.com/emad-elsaid/xlog/extensions/sql_table"
36 | _ "github.com/emad-elsaid/xlog/extensions/star"
37 | _ "github.com/emad-elsaid/xlog/extensions/toc"
38 | _ "github.com/emad-elsaid/xlog/extensions/todo"
39 | _ "github.com/emad-elsaid/xlog/extensions/upload_file"
40 | )
41 |
--------------------------------------------------------------------------------
/extensions/autolink/autolink.go:
--------------------------------------------------------------------------------
1 | package autolink
2 |
3 | import (
4 | "bytes"
5 |
6 | . "github.com/emad-elsaid/xlog"
7 | "github.com/yuin/goldmark/ast"
8 | "github.com/yuin/goldmark/renderer"
9 | "github.com/yuin/goldmark/util"
10 | )
11 |
12 | func init() {
13 | RegisterExtension(AutoLink{})
14 | }
15 |
16 | type AutoLink struct{}
17 |
18 | func (AutoLink) Name() string { return "autolink" }
19 | func (AutoLink) Init() {
20 | MarkdownConverter().Renderer().AddOptions(renderer.WithNodeRenderers(
21 | util.Prioritized(&extension{}, -1),
22 | ))
23 | }
24 |
25 | type extension struct{}
26 |
27 | func (h *extension) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
28 | reg.Register(ast.KindAutoLink, render)
29 | }
30 |
31 | func render(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
32 | n := node.(*ast.AutoLink)
33 | if !entering {
34 | return ast.WalkContinue, nil
35 | }
36 | _, _ = w.WriteString(`')
51 | } else {
52 | _, _ = w.WriteString(`">`)
53 | }
54 | _, _ = w.Write(util.EscapeHTML(label))
55 | _, _ = w.WriteString(``)
56 | return ast.WalkContinue, nil
57 | }
58 |
--------------------------------------------------------------------------------
/extensions/autolink_pages/Autolink pages.md:
--------------------------------------------------------------------------------
1 | #extension
2 |
3 | Autolink pages extension converts any text that matches another page title to a link automatically.
4 | For example just by writing (xlog), it links to another page called `xlog.md`.
5 | So as a writer you don't have to go back retroactively and add links to your page in older pages.
6 | Also it keeps backlinks to the current page and show a list of them under the page for quick access to relevant pages.
7 |
--------------------------------------------------------------------------------
/extensions/autolink_pages/autolink_pages.go:
--------------------------------------------------------------------------------
1 | package autolink_pages
2 |
3 | import (
4 | "context"
5 | "embed"
6 | "html/template"
7 | "path"
8 | "sort"
9 | "strings"
10 | "sync"
11 |
12 | _ "embed"
13 |
14 | . "github.com/emad-elsaid/xlog"
15 | "github.com/yuin/goldmark/ast"
16 | east "github.com/yuin/goldmark/extension/ast"
17 | )
18 |
19 | //go:embed templates
20 | var templates embed.FS
21 |
22 | type NormalizedPage struct {
23 | page Page
24 | normalizedName string
25 | }
26 |
27 | type fileInfoByNameLength []*NormalizedPage
28 |
29 | func (a fileInfoByNameLength) Len() int { return len(a) }
30 | func (a fileInfoByNameLength) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
31 | func (a fileInfoByNameLength) Less(i, j int) bool {
32 | return len(a[i].normalizedName) > len(a[j].normalizedName)
33 | }
34 |
35 | var autolinkPages []*NormalizedPage
36 | var autolinkPage_lck sync.Mutex
37 |
38 | func UpdatePagesList(Page) (err error) {
39 | autolinkPage_lck.Lock()
40 | defer autolinkPage_lck.Unlock()
41 |
42 | ps := MapPage(context.Background(), func(p Page) *NormalizedPage {
43 | return &NormalizedPage{
44 | page: p,
45 | normalizedName: path.Base(strings.ToLower(p.Name())),
46 | }
47 | })
48 | sort.Sort(fileInfoByNameLength(ps))
49 | autolinkPages = ps
50 | return
51 | }
52 |
53 | func countTodos(p Page) (total int, done int) {
54 | _, tree := p.AST()
55 | tasks := FindAllInAST[*east.TaskCheckBox](tree)
56 | for _, v := range tasks {
57 | total++
58 | if v.IsChecked {
59 | done++
60 | }
61 | }
62 |
63 | return
64 | }
65 |
66 | func backlinksSection(p Page) template.HTML {
67 | if p.Name() == Config.Index {
68 | return ""
69 | }
70 |
71 | pages := MapPage(context.Background(), func(a Page) Page {
72 | _, tree := a.AST()
73 | if a.Name() == p.Name() || !containLinkTo(tree, p) {
74 | return nil
75 | }
76 |
77 | return a
78 | })
79 |
80 | return Partial("backlinks", Locals{"pages": pages})
81 | }
82 |
83 | func containLinkTo(n ast.Node, p Page) bool {
84 | if n.Kind() == KindPageLink {
85 | t, _ := n.(*PageLink)
86 | if t.page.FileName() == p.FileName() {
87 | return true
88 | }
89 | }
90 | if n.Kind() == ast.KindLink {
91 | t, _ := n.(*ast.Link)
92 | dst := string(t.Destination)
93 |
94 | // link is absolute: remove /
95 | if strings.HasPrefix(dst, "/") {
96 | path := strings.TrimPrefix(dst, "/")
97 | if string(path) == p.Name() {
98 | return true
99 | }
100 | } else { // link is relative: get relative part
101 | // TODO: what if another folder has the same filename?
102 | // * just ignore that fact
103 | // * dont support relative paths
104 | // there is no way to know who is the parent folder
105 | base := path.Base(p.Name())
106 | if dst == base {
107 | return true
108 | }
109 | }
110 | }
111 |
112 | for c := n.FirstChild(); c != nil; c = c.NextSibling() {
113 | if containLinkTo(c, p) {
114 | return true
115 | }
116 |
117 | if c == n.LastChild() {
118 | break
119 | }
120 | }
121 |
122 | return false
123 | }
124 |
--------------------------------------------------------------------------------
/extensions/autolink_pages/extension.go:
--------------------------------------------------------------------------------
1 | package autolink_pages
2 |
3 | import (
4 | . "github.com/emad-elsaid/xlog"
5 | "github.com/yuin/goldmark/parser"
6 | "github.com/yuin/goldmark/renderer"
7 | "github.com/yuin/goldmark/util"
8 | )
9 |
10 | func init() {
11 | RegisterExtension(AutoLinkPages{})
12 | }
13 |
14 | type AutoLinkPages struct{}
15 |
16 | func (AutoLinkPages) Name() string { return "autolink-pages" }
17 | func (AutoLinkPages) Init() {
18 | if !Config.Readonly {
19 | Listen(PageChanged, UpdatePagesList)
20 | Listen(PageDeleted, UpdatePagesList)
21 | }
22 |
23 | RegisterWidget(WidgetAfterView, 1, backlinksSection)
24 | RegisterTemplate(templates, "templates")
25 | MarkdownConverter().Parser().AddOptions(parser.WithInlineParsers(
26 | util.Prioritized(&pageLinkParser{}, 999),
27 | ))
28 | MarkdownConverter().Renderer().AddOptions(renderer.WithNodeRenderers(
29 | util.Prioritized(&pageLinkRenderer{}, -1),
30 | ))
31 | }
32 |
--------------------------------------------------------------------------------
/extensions/autolink_pages/node.go:
--------------------------------------------------------------------------------
1 | package autolink_pages
2 |
3 | import (
4 | . "github.com/emad-elsaid/xlog"
5 | "github.com/yuin/goldmark/ast"
6 | )
7 |
8 | var KindPageLink = ast.NewNodeKind("PageLink")
9 |
10 | type PageLink struct {
11 | ast.BaseInline
12 | page Page
13 | }
14 |
15 | func (*PageLink) Kind() ast.NodeKind {
16 | return KindPageLink
17 | }
18 |
19 | func (p *PageLink) Dump(source []byte, level int) {
20 | m := map[string]string{
21 | "value": p.page.Name(),
22 | }
23 | ast.DumpHelper(p, source, level, m, nil)
24 | }
25 |
--------------------------------------------------------------------------------
/extensions/autolink_pages/parser.go:
--------------------------------------------------------------------------------
1 | package autolink_pages
2 |
3 | import (
4 | "strings"
5 |
6 | . "github.com/emad-elsaid/xlog"
7 | "github.com/yuin/goldmark/ast"
8 | "github.com/yuin/goldmark/parser"
9 | "github.com/yuin/goldmark/text"
10 | "github.com/yuin/goldmark/util"
11 | )
12 |
13 | type pageLinkParser struct{}
14 |
15 | func (*pageLinkParser) Trigger() []byte {
16 | // ' ' indicates any white spaces and a line head
17 | return []byte{' ', '*', '_', '~', '('}
18 | }
19 |
20 | func (s *pageLinkParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
21 | if pc.IsInLinkLabel() {
22 | return nil
23 | }
24 |
25 | if autolinkPages == nil {
26 | UpdatePagesList(nil)
27 | }
28 |
29 | line, segment := block.PeekLine()
30 | if line == nil {
31 | return nil
32 | }
33 |
34 | consumes := 0
35 | start := segment.Start
36 | c := line[0]
37 | // advance if current position is not a line head.
38 | if c == ' ' || c == '*' || c == '_' || c == '~' || c == '(' {
39 | consumes++
40 | start++
41 | line = line[1:]
42 | }
43 |
44 | var found Page
45 | var m int
46 | normalizedLine := strings.ToLower(string(line))
47 |
48 | for _, p := range autolinkPages {
49 | if len(line) < len(p.normalizedName) {
50 | continue
51 | }
52 |
53 | // Found a page
54 | if strings.HasPrefix(normalizedLine, p.normalizedName) {
55 | found = p.page
56 | m = len(p.normalizedName)
57 | break
58 | }
59 | }
60 |
61 | if found == nil ||
62 | (len(line) > m && util.IsAlphaNumeric(line[m])) { // next character is word character
63 | block.Advance(consumes)
64 | return nil
65 | }
66 |
67 | if consumes != 0 {
68 | s := segment.WithStop(segment.Start + 1)
69 | ast.MergeOrAppendTextSegment(parent, s)
70 | }
71 | consumes += m
72 | block.Advance(consumes)
73 |
74 | n := ast.NewTextSegment(text.NewSegment(start, start+m))
75 | link := &PageLink{
76 | page: found,
77 | }
78 | link.AppendChild(link, n)
79 | return link
80 | }
81 |
--------------------------------------------------------------------------------
/extensions/autolink_pages/renderer.go:
--------------------------------------------------------------------------------
1 | package autolink_pages
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/yuin/goldmark/ast"
7 | "github.com/yuin/goldmark/renderer"
8 | "github.com/yuin/goldmark/util"
9 | )
10 |
11 | type pageLinkRenderer struct{}
12 |
13 | func (h *pageLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
14 | reg.Register(KindPageLink, render)
15 | }
16 |
17 | func render(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
18 | if entering {
19 | n := node.(*PageLink)
20 | url := n.page.Name()
21 |
22 | fmt.Fprintf(w,
23 | ``,
24 | util.EscapeHTML(util.URLEscape([]byte([]byte(url)), false)),
25 | )
26 |
27 | if total, done := countTodos(n.page); total > 0 {
28 | isDone := ""
29 | if total == done {
30 | isDone = "is-success"
31 | }
32 | fmt.Fprintf(w, `%d/%d `, isDone, done, total)
33 | }
34 | } else {
35 | w.WriteString(``)
36 | }
37 |
38 | return ast.WalkContinue, nil
39 | }
40 |
--------------------------------------------------------------------------------
/extensions/autolink_pages/templates/backlinks.html:
--------------------------------------------------------------------------------
1 | {{ if .pages }}
2 |
14 | {{ end }}
15 |
--------------------------------------------------------------------------------
/extensions/search/templates/search.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/extensions/shortcode/ShortCode.md:
--------------------------------------------------------------------------------
1 | 
2 | #extension
3 |
4 | Xlog extension **shortcode** allow using blocks with custom language code that can render the content of the block with custom function
5 |
6 | For example rendering an alert can use two different formats
7 |
8 | # Short format
9 |
10 |
11 | /alert this is important
12 |
13 |
14 | /alert this is important
15 |
16 | # Long format
17 |
18 |
19 | ```alert
20 | this is important
21 | ```
22 |
23 |
24 | ```alert
25 | this is important
26 | ```
27 |
28 | # Default blocks
29 |
30 | Shortcode extension includes couple default blocks:
31 |
32 | ## /alert
33 |
34 | ```alert
35 | Computer science is the study of computation, automation, and information. Computer science spans theoretical disciplines (such as algorithms, theory of computation, information theory, and automation) to practical disciplines (including the design and implementation of hardware and software). Computer science is generally considered an area of academic research and distinct from computer programming.
36 | ```
37 |
38 | ## /info
39 |
40 | ```info
41 | Computer science is the study of computation, automation, and information. Computer science spans theoretical disciplines (such as algorithms, theory of computation, information theory, and automation) to practical disciplines (including the design and implementation of hardware and software). Computer science is generally considered an area of academic research and distinct from computer programming.
42 | ```
43 |
44 | ## /success
45 |
46 | ```success
47 | Computer science is the study of computation, automation, and information. Computer science spans theoretical disciplines (such as algorithms, theory of computation, information theory, and automation) to practical disciplines (including the design and implementation of hardware and software). Computer science is generally considered an area of academic research and distinct from computer programming.
48 | ```
49 |
50 | ## /warning
51 |
52 | ```warning
53 | Computer science is the study of computation, automation, and information. Computer science spans theoretical disciplines (such as algorithms, theory of computation, information theory, and automation) to practical disciplines (including the design and implementation of hardware and software). Computer science is generally considered an area of academic research and distinct from computer programming.
54 | ```
55 |
--------------------------------------------------------------------------------
/extensions/shortcode/extension.go:
--------------------------------------------------------------------------------
1 | package shortcode
2 |
3 | import (
4 | . "github.com/emad-elsaid/xlog"
5 | "github.com/yuin/goldmark/parser"
6 | "github.com/yuin/goldmark/renderer"
7 | "github.com/yuin/goldmark/util"
8 | )
9 |
10 | func init() {
11 | RegisterExtension(ShortCodeEx{})
12 | }
13 |
14 | type ShortCodeEx struct{}
15 |
16 | func (ShortCodeEx) Name() string { return "shortcode" }
17 | func (ShortCodeEx) Init() {
18 | MarkdownConverter().Parser().AddOptions(parser.WithBlockParsers(
19 | util.Prioritized(&shortCodeParser{}, 0),
20 | ))
21 | MarkdownConverter().Renderer().AddOptions(renderer.WithNodeRenderers(
22 | util.Prioritized(&shortCodeRenderer{}, 0),
23 | ))
24 | MarkdownConverter().Parser().AddOptions(
25 | parser.WithASTTransformers(
26 | util.Prioritized(transformShortCodeBlocks{}, 0),
27 | ),
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/extensions/shortcode/node.go:
--------------------------------------------------------------------------------
1 | package shortcode
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/yuin/goldmark/ast"
7 | )
8 |
9 | var KindShortCode = ast.NewNodeKind("ShortCode")
10 |
11 | type ShortCodeNode struct {
12 | ast.BaseBlock
13 | start int
14 | end int
15 | fun ShortCode
16 | }
17 |
18 | func (s *ShortCodeNode) Dump(source []byte, level int) {
19 | m := map[string]string{
20 | "value": fmt.Sprintf("%#v", s),
21 | }
22 | ast.DumpHelper(s, source, level, m, nil)
23 | }
24 |
25 | func (h *ShortCodeNode) Kind() ast.NodeKind {
26 | return KindShortCode
27 | }
28 |
29 | var KindShortCodeBlock = ast.NewNodeKind("ShortCodeBlock")
30 |
31 | type ShortCodeBlock struct {
32 | ast.FencedCodeBlock
33 | fun ShortCode
34 | }
35 |
36 | func (s *ShortCodeBlock) Kind() ast.NodeKind {
37 | return KindShortCodeBlock
38 | }
39 |
--------------------------------------------------------------------------------
/extensions/shortcode/parser.go:
--------------------------------------------------------------------------------
1 | package shortcode
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/yuin/goldmark/ast"
7 | "github.com/yuin/goldmark/parser"
8 | "github.com/yuin/goldmark/text"
9 | )
10 |
11 | const trigger = '/'
12 |
13 | type shortCodeParser struct{}
14 |
15 | func (s *shortCodeParser) Trigger() []byte {
16 | return []byte{trigger}
17 | }
18 |
19 | func (s *shortCodeParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
20 | l, seg := reader.PeekLine()
21 | line := string(l)
22 | if len(line) == 0 || line[0] != trigger {
23 | return nil, parser.Close
24 | }
25 |
26 | endOfShortcode := strings.IndexAny(line, " \n")
27 | if endOfShortcode == -1 {
28 | endOfShortcode = len(line)
29 | }
30 |
31 | firstWord := line[1:endOfShortcode]
32 | var processor ShortCode
33 | var ok bool
34 | if processor, ok = shortcodes[firstWord]; !ok {
35 | return nil, parser.Close
36 | }
37 |
38 | reader.AdvanceLine()
39 |
40 | firstSpace := strings.IndexAny(line, " ")
41 | if firstSpace == -1 {
42 | return &ShortCodeNode{
43 | start: seg.Stop,
44 | end: seg.Stop,
45 | fun: processor,
46 | }, parser.Close
47 | }
48 |
49 | return &ShortCodeNode{
50 | start: seg.Start + endOfShortcode,
51 | end: seg.Stop,
52 | fun: processor,
53 | }, parser.Close
54 | }
55 |
56 | func (s *shortCodeParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
57 | return parser.Close
58 | }
59 |
60 | func (s *shortCodeParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {}
61 | func (s *shortCodeParser) CanInterruptParagraph() bool { return true }
62 | func (s *shortCodeParser) CanAcceptIndentedLine() bool { return false }
63 |
--------------------------------------------------------------------------------
/extensions/shortcode/parser_test.go:
--------------------------------------------------------------------------------
1 | package shortcode_test
2 |
3 | import (
4 | "bytes"
5 | "html/template"
6 | "testing"
7 |
8 | "github.com/emad-elsaid/xlog"
9 | "github.com/emad-elsaid/xlog/extensions/shortcode"
10 | )
11 |
12 | func TestShortCode(t *testing.T) {
13 | tcs := []struct {
14 | name string
15 | input string
16 | handlerOutput string
17 | output string
18 | }{
19 | {
20 | name: "page with one line",
21 | input: "/test",
22 | handlerOutput: "output",
23 | output: "output",
24 | },
25 | {
26 | name: "short code with new line before it",
27 | input: "\n/test",
28 | handlerOutput: "output",
29 | output: "output",
30 | },
31 | {
32 | name: "short code with new line after it",
33 | input: "/test\n",
34 | handlerOutput: "output",
35 | output: "output",
36 | },
37 | {
38 | name: "short code with new line before and after it",
39 | input: "\n/test\n",
40 | handlerOutput: "output",
41 | output: "output",
42 | },
43 | {
44 | name: "short code with space after",
45 | input: "/test ",
46 | handlerOutput: "output",
47 | output: "output",
48 | },
49 | {
50 | name: "two short codes",
51 | input: "/test\n\n/test",
52 | handlerOutput: "output",
53 | output: "outputoutput",
54 | },
55 | }
56 |
57 | for _, tc := range tcs {
58 | t.Run(tc.name, func(t *testing.T) {
59 | handler := func(xlog.Markdown) template.HTML { return template.HTML(tc.handlerOutput) }
60 | shortcode.RegisterShortCode("test", shortcode.ShortCode{Render: handler, Default: ""})
61 |
62 | output := bytes.NewBufferString("")
63 | xlog.MarkdownConverter().Convert([]byte(tc.input), output)
64 | if output.String() != tc.output {
65 | t.Errorf("input: %s\nexpected: %s\noutput: %s", tc.input, tc.output, output.String())
66 | }
67 | })
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/extensions/shortcode/renderer.go:
--------------------------------------------------------------------------------
1 | package shortcode
2 |
3 | import (
4 | . "github.com/emad-elsaid/xlog"
5 | "github.com/yuin/goldmark/ast"
6 | "github.com/yuin/goldmark/renderer"
7 | "github.com/yuin/goldmark/util"
8 | )
9 |
10 | type shortCodeRenderer struct{}
11 |
12 | func (s *shortCodeRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
13 | reg.Register(KindShortCode, s.render)
14 | reg.Register(KindShortCodeBlock, s.renderBlock)
15 | }
16 |
17 | func (s *shortCodeRenderer) render(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
18 | if !entering {
19 | return ast.WalkContinue, nil
20 | }
21 |
22 | node, ok := n.(*ShortCodeNode)
23 | if !ok {
24 | return ast.WalkContinue, nil
25 | }
26 |
27 | content := source[node.start:node.end]
28 | output := node.fun.Render(Markdown(content))
29 | w.Write([]byte(output))
30 |
31 | return ast.WalkContinue, nil
32 | }
33 |
34 | func (s *shortCodeRenderer) renderBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
35 | if !entering {
36 | return ast.WalkContinue, nil
37 | }
38 |
39 | node, ok := n.(*ShortCodeBlock)
40 | if !ok {
41 | return ast.WalkContinue, nil
42 | }
43 |
44 | lines := node.Lines()
45 | content := ""
46 | for i := 0; i < lines.Len(); i++ {
47 | line := lines.At(i)
48 | content += string(line.Value(source))
49 | }
50 |
51 | output := node.fun.Render(Markdown(content))
52 | w.Write([]byte(output))
53 |
54 | return ast.WalkContinue, nil
55 | }
56 |
--------------------------------------------------------------------------------
/extensions/shortcode/shortcode.go:
--------------------------------------------------------------------------------
1 | package shortcode
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "html/template"
7 |
8 | . "github.com/emad-elsaid/xlog"
9 | )
10 |
11 | type ShortCode struct {
12 | Render func(Markdown) template.HTML
13 | Default string
14 | }
15 |
16 | func render(i Markdown) string {
17 | var b bytes.Buffer
18 | MarkdownConverter().Convert([]byte(i), &b)
19 | return b.String()
20 | }
21 |
22 | func container(cls string, content Markdown) template.HTML {
23 | tpl := `
90 |
91 |
96 |
97 |
98 | {{ template "footer" . }}
99 |
--------------------------------------------------------------------------------
/templates/pages-grid.html:
--------------------------------------------------------------------------------
1 |
29 |
--------------------------------------------------------------------------------
/templates/pages.html:
--------------------------------------------------------------------------------
1 |
26 |
--------------------------------------------------------------------------------
/tutorials/Creating a site.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | #tutorial
4 |
5 | * Xlog doesn't require custom structure for your markdown files
6 | * Default main file name is `index.md` and can be overriden with `--index` flag
7 |
8 | # Create empty directory
9 |
10 | Create a new empty directory and `cd` into it or simple navigate to existing directory which has markdown files
11 |
12 | ```shell
13 | mkdir myblog
14 | cd myblog
15 | ```
16 |
17 | # Run Xlog
18 |
19 | Assuming you already went through one of the Installation methods. `xlog` should be in your **PATH**. Simply executing it in current directory starts an HTTP server on port 3000
20 |
21 | ```shell
22 | xlog
23 | ```
24 |
25 | # Running on a different port
26 |
27 | The previous command starts a server on port **3000** if you want to specify the port you can do so using `--bind` flag
28 |
29 | ```shell
30 | xlog --bind 127.0.0.1:4000
31 | ```
32 |
33 | This will run the server on port **4000** instead of **3000**
34 |
35 | # Using a different index page
36 |
37 | Xlog assumes the main page is **index.md** if you're working in an existing github repository for example you may need to specify **README.md** as your index page as follows
38 |
39 | ```shell
40 | xlog --index README
41 | ```
42 |
43 | Notice that specifying the index page doesn't need the extension `.md`.
44 |
45 | # Open your new site
46 |
47 | Now you can navigate to [http://localhost:3000](http://localhost:3000) in your browser to start browsing the markdown files. if it's a new directory your editor will open to write your first page.
48 |
49 | # Generating a static site
50 |
51 | You can generate HTML files from your markdown files using `--build` flag
52 |
53 | ```shell
54 | xlog --build .
55 | ```
56 |
57 | Which will convert all of your markdown files to HTML files in the current directory.
58 |
59 | You can specify a destination for the HTML output.
60 |
61 | ```shell
62 | xlog --build /destination/directory/path
63 | ```
64 |
65 | # Integration with Github pages
66 |
67 | If your markdown is hosted as Gituhub repository. You can use github workflows to download and execute xlog to generate HTML pages and host it with github pages.
68 |
69 | Tutorial can be found in Create your own digital garden on Github and Examples can be found here:
70 | - [Emad Elsaid Blog](https://github.com/emad-elsaid/emad-elsaid.github.io/blob/master/.github/workflows/xlog.yml)
71 | - [Xlog documentation](https://github.com/emad-elsaid/xlog/blob/master/.github/workflows/xlog.yml)
72 |
--------------------------------------------------------------------------------
/tutorials/Custom installation.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | #tutorial
4 |
5 | Xlog ships with a CLI that includes the core and all official extensions. There are cases where you need custom set of extensions:
6 |
7 | * Maybe you need only the core features without any extension
8 | * Maybe there is an extension that you don't need or want or misbehaving
9 | * Maybe you developed a set of extensions and you want to include them in your installations
10 |
11 | Here is how you can build your own custom xlog with features you select.
12 |
13 | # Creating a Go module
14 |
15 | Create a directory for your custom installation and initialize a go module in it.
16 |
17 | ```shell
18 | mkdir custom_xlog
19 | cd custom_xlog
20 | go mod init github.com/yourusername/custom_xlog
21 | ```
22 |
23 | # Main file
24 |
25 | Then create a file `xlog.go` for example with the following content
26 |
27 | ```go
28 | package main
29 |
30 | import (
31 | // Core
32 | "github.com/emad-elsaid/xlog"
33 |
34 | // All official extensions
35 | _ "github.com/emad-elsaid/xlog/extensions/all"
36 | )
37 |
38 | func main() {
39 | xlog.Start()
40 | }
41 | ```
42 |
43 | # Selecting extensions
44 |
45 | The previous file is what xlog ships in `cmd/xlog/xlog.go` if you missed up at any point feel free to go back to it and copy it from there.
46 |
47 | If you want to select specific extensions you can replace `extensions/all` line with a list of extensions that you want.
48 |
49 | All extensions are imported to [`extensions/all/all.go`](https://github.com/emad-elsaid/xlog/blob/master/extensions/all/all.go). feel free to copy any of them as needed.
50 |
51 | You can also import any extensions that you developed at this point.
52 |
53 | # Running your custom xlog
54 |
55 | Now use Go to run your custom installation
56 |
57 | ```shell
58 | go get github.com/emad-elsaid/xlog
59 | go run xlog.go
60 | ```
61 |
62 |
--------------------------------------------------------------------------------
/tutorials/Generate static website.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | #tutorial
4 |
5 | Xlog CLI allow for generating static website from source directory. this is how this website is generated.
6 |
7 | To generate a static website using Xlog use the `--build` flag with a path as destination for example:
8 |
9 | ```shell
10 | xlog --build /path/to/output
11 | ```
12 |
13 | Xlog will build all markdown files to HTML and extract all static files from inside the binary executable file to that destination directory. Then it will terminate.
14 |
15 | Building process creates a xlog server instance and request all pages and save it to desk. That allow xlog extensions to define a new handler that renders a page. the page will work in both usecases: local server, static site generation. extensions has to also register the path for build using [`RegisteBuildPage`](https://pkg.go.dev/github.com/emad-elsaid/xlog#RegisterBuildPage) function
16 |
17 | While building static site xlog turns on **READONLY** mode.
18 |
19 |
--------------------------------------------------------------------------------
/widgets.go:
--------------------------------------------------------------------------------
1 | package xlog
2 |
3 | import (
4 | "html/template"
5 | "iter"
6 | "sort"
7 | )
8 |
9 | type (
10 | // WidgetSpace used to represent a widgets spaces. it's used to register
11 | // widgets to be injected in the view or edit pages
12 | WidgetSpace string
13 | // WidgetFunc a function that takes the current page and returns the widget.
14 | // This can be used by extensions to define new widgets to be rendered in
15 | // view or edit pages. the extension should define this func type and
16 | // register it to be rendered in a specific widgetSpace such as before or
17 | // after the page.
18 | WidgetFunc func(Page) template.HTML
19 | )
20 |
21 | // List of widgets spaces that extensions can use to register a WidgetFunc to
22 | // inject content into.
23 | var (
24 | WidgetAfterView WidgetSpace = "after_view" // widgets rendered after the content of the view page
25 | WidgetBeforeView WidgetSpace = "before_view" // widgets rendered before the content of the view page
26 | WidgetHead WidgetSpace = "head" // widgets rendered in page tag
27 | )
28 |
29 | // A map to keep track of list of widget functions registered in each widget space
30 | var widgets = map[WidgetSpace]*priorityList[WidgetFunc]{}
31 |
32 | // RegisterWidget Register a function to a widget space. functions registered
33 | // will be executed in order of priority lower to higher when rendering view or
34 | // edit page. the return values of these widgetfuncs will pass down to the
35 | // template and injected in reserved places.
36 | func RegisterWidget(s WidgetSpace, priority float32, f WidgetFunc) {
37 | pl, ok := widgets[s]
38 | if !ok {
39 | pl = new(priorityList[WidgetFunc])
40 | widgets[s] = pl
41 | }
42 |
43 | pl.Add(f, priority)
44 | }
45 |
46 | // This is used by view and edit routes to render all widgetfuncs registered for
47 | // specific widget space.
48 | func RenderWidget(s WidgetSpace, p Page) (o template.HTML) {
49 | w, ok := widgets[s]
50 | if !ok {
51 | return
52 | }
53 |
54 | for f := range w.All() {
55 | o += f(p)
56 | }
57 | return
58 | }
59 |
60 | type priorityItem[T any] struct {
61 | Item T
62 | Priority float32
63 | }
64 |
65 | type priorityList[T any] struct {
66 | items []priorityItem[T]
67 | }
68 |
69 | func (pl *priorityList[T]) Add(item T, priority float32) {
70 | pl.items = append(pl.items, priorityItem[T]{Item: item, Priority: priority})
71 | pl.sortByPriority()
72 | }
73 |
74 | func (pl *priorityList[T]) sortByPriority() {
75 | sort.Slice(pl.items, func(i, j int) bool {
76 | return pl.items[i].Priority < pl.items[j].Priority
77 | })
78 | }
79 |
80 | // An iterator over all items
81 | func (pl *priorityList[T]) All() iter.Seq[T] {
82 | return func(yield func(T) bool) {
83 | for _, v := range pl.items {
84 | if !yield(v.Item) {
85 | return
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------