From 2218a678b1ec8f68c2ec2d48a300783fe44f306f Mon Sep 17 00:00:00 2001 From: Adam Piontek Date: Mon, 5 Apr 2021 16:51:59 -0400 Subject: [PATCH] syntax highlighting and blog/post liveviews working; numerous other styling updates --- .gitignore | 3 + .iex.exs | 1 + README.md | 31 +- assets/css/_bs-custom.scss | 8 +- assets/css/_fonts.scss | 48 ++-- assets/css/_pygments.css | 269 ++++++++++++++++++ assets/css/app.scss | 39 ++- assets/js/app.js | 3 + assets/package-lock.json | 133 +++++++-- assets/package.json | 5 +- assets/webpack.config.js | 2 +- config/config.exs | 5 +- lib/home73k.ex | 6 +- lib/home73k/blog.ex | 4 +- lib/home73k/blog/post.ex | 16 +- lib/home73k/highlighter.ex | 52 ++++ lib/home73k/temp.ex | 19 ++ lib/home73k_web.ex | 3 + lib/home73k_web/live/blog_live.ex | 3 - lib/home73k_web/live/blog_live.html.leex | 14 +- lib/home73k_web/live/post_live.ex | 3 - lib/home73k_web/live/post_live.html.leex | 10 +- lib/home73k_web/templates/home/about.html.eex | 2 +- lib/home73k_web/templates/home/folio.html.eex | 2 +- lib/home73k_web/templates/home/index.html.eex | 2 +- .../templates/home/resume.html.eex | 2 +- lib/home73k_web/views/date_helpers.ex | 9 + mix.lock | 1 + .../content/2018/03/2018-03-24_pihole-love.md | 2 +- ...-17_if-no-one-will-do-it-neednt-be-done.md | 2 +- ...lana-del-arr-ultrapirates-parody-lyrics.md | 2 +- .../05/2020-05-23_creating-gif-from-video.md | 2 +- .../08/2020-08-01_enable-vs-cli-env-in-ps.md | 2 +- .../2020-12-07_shame-american-leadership.md | 23 ++ .../2020/12/2020-12-29_moms-meatloaf.md | 2 +- 35 files changed, 610 insertions(+), 120 deletions(-) create mode 100644 assets/css/_pygments.css create mode 100644 lib/home73k/highlighter.ex create mode 100644 lib/home73k/temp.ex create mode 100644 lib/home73k_web/views/date_helpers.ex create mode 100644 priv/content/2020/12/2020-12-07_shame-american-leadership.md diff --git a/.gitignore b/.gitignore index de96d9a..c16d981 100644 --- a/.gitignore +++ b/.gitignore @@ -39,5 +39,8 @@ npm-debug.log # for vscode elixir_ls extension files /.elixir_ls +# Ignore the pygments venv directory +/priv/pygments/ + # dev TODO.md diff --git a/.iex.exs b/.iex.exs index 701cbda..0e09382 100644 --- a/.iex.exs +++ b/.iex.exs @@ -1,2 +1,3 @@ +alias Home73k.Temp alias Home73k.Blog alias Home73k.Blog.Post diff --git a/README.md b/README.md index eeafdf6..b18fcce 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,26 @@ # Home73k -To start your Phoenix server: +Personal website with blog. - * Install dependencies with `mix deps.get` - * Install Node.js dependencies with `npm install` inside the `assets` directory - * Start Phoenix endpoint with `mix phx.server` +## Blog posts -Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. +Posts are markdown files stored under `priv/content` and parsed by [Earmark](https://hexdocs.pm/earmark/Earmark.html). This can be configured in `config.exs` under `config :home73k, :app_global_vars, blog_content: "path/to/content"` -Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). +### Syntax highlighting -## Learn more +For the challenge of it, and to keep user's browsers from having to run javascript just to highlight some code, I chose to do server-side syntax highlighting. - * Official website: https://www.phoenixframework.org/ - * Guides: https://hexdocs.pm/phoenix/overview.html - * Docs: https://hexdocs.pm/phoenix - * Forum: https://elixirforum.com/c/phoenix-forum - * Source: https://github.com/phoenixframework/phoenix +Due to the lexer limitations of elixir-native solutions, the highlighter uses [Pygments](https://pygments.org/) by calling [pygmentize](https://pygments.org/docs/cmdline/) via [System.cmd](https://hexdocs.pm/elixir/System.html#cmd/3) + +However, this requires installing python3 & Pygments. Best way to do this is with a venv (virtual python environment), so you'll also want `python3-venv` installed on a debian system, for example. + +By default, Home73k is configured to look for pygmentize in a venv at `priv/pygments/bin/pygmentize` -- here's a quick howto for how to set that up: + +```shell +cd priv +python3 -m venv pygments +source pygments/bin/activate +pip3 install Pygments +``` + +The location of bin/pygmentize can be configured in `config.exs` under `config :home73k, :app_global_vars, pygmentize_bin: "path/to/bin/pygmentize"` diff --git a/assets/css/_bs-custom.scss b/assets/css/_bs-custom.scss index a211a99..0fb2442 100644 --- a/assets/css/_bs-custom.scss +++ b/assets/css/_bs-custom.scss @@ -4,8 +4,8 @@ // // Font, line-height, and color for body text, headings, and more. -// stylelint-disable value-keyword-case -$font-family-sans-serif: "Work Sans", system-ui, -apple-system, "Segoe UI", Roboto, +$font-size-base: 1.125rem; +$font-family-sans-serif: "Open Sans", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !important; $font-family-base: $font-family-sans-serif; @@ -14,10 +14,8 @@ $font-family-brand: Righteous, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !important; -$font-family-monospace: "Fira Mono", SFMono-Regular, Menlo, Monaco, Consolas, +$font-family-monospace: "Fira Code", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; -$font-family-code: "Fira Code", "Fira Mono", SFMono-Regular, Menlo, Monaco, - Consolas, "Liberation Mono", "Courier New", monospace !important; // Features $enable-shadows: true; diff --git a/assets/css/_fonts.scss b/assets/css/_fonts.scss index 145e8d2..a96629e 100644 --- a/assets/css/_fonts.scss +++ b/assets/css/_fonts.scss @@ -1,35 +1,23 @@ -/* Fontsource Work Sans */ -@import "../node_modules/@fontsource/work-sans/100.css"; /* thin | normal */ -@import "../node_modules/@fontsource/work-sans/100-italic.css"; /* thin | italic */ -// @import "../node_modules/@fontsource/work-sans/200.css"; /* thin-light | normal */ -// @import "../node_modules/@fontsource/work-sans/200-italic.css"; /* thin-light | italic */ -@import "../node_modules/@fontsource/work-sans/300.css"; /* light | normal */ -@import "../node_modules/@fontsource/work-sans/300-italic.css"; /* light | italic */ -@import "../node_modules/@fontsource/work-sans/400.css"; /* normal | normal */ -@import "../node_modules/@fontsource/work-sans/400-italic.css"; /* normal | italic */ -// @import "../node_modules/@fontsource/work-sans/500.css"; /* heavier | normal */ -// @import "../node_modules/@fontsource/work-sans/500-italic.css"; /* heavier | italic */ -@import "../node_modules/@fontsource/work-sans/600.css"; /* heavier? | normal */ -@import "../node_modules/@fontsource/work-sans/600-italic.css"; /* heavier | italic */ -@import "../node_modules/@fontsource/work-sans/700.css"; /* bold | normal */ -@import "../node_modules/@fontsource/work-sans/700-italic.css"; /* bold | italic */ -// @import "../node_modules/@fontsource/work-sans/800.css"; /* bolder? | normal */ -// @import "../node_modules/@fontsource/work-sans/800-italic.css"; /* bolder? | italic */ -// @import "../node_modules/@fontsource/work-sans/900.css"; /* black | normal */ -// @import "../node_modules/@fontsource/work-sans/900-italic.css"; /* black | italic */ - - -/* Fontsource Righteous */ +/* brand : Fontsource Righteous */ @import "../node_modules/@fontsource/righteous/400.css"; /* normal | normal */ -/* Fontsource Fira Mono */ -@import "../node_modules/@fontsource/fira-mono/400.css"; /* normal | normal */ -@import "../node_modules/@fontsource/fira-mono/500.css"; /* heavier normal? */ -// @import "../node_modules/@fontsource/fira-mono/700.css"; /* bold | normal */ - -/* Fontsource Fira Code */ +/* monospace : Fontsource Fira Code */ @import "../node_modules/@fontsource/fira-code/300.css"; /* light | normal */ @import "../node_modules/@fontsource/fira-code/400.css"; /* normal | normal */ -// @import "../node_modules/@fontsource/fira-code/500.css"; /* heavier normal? */ -// @import "../node_modules/@fontsource/fira-code/600.css"; /* heavier normal? */ +@import "../node_modules/@fontsource/fira-code/500.css"; /* heavier normal? */ +@import "../node_modules/@fontsource/fira-code/600.css"; /* heavier normal? */ @import "../node_modules/@fontsource/fira-code/700.css"; /* bold | normal */ + + +/* Fontsource Open Sans */ +@import "../node_modules/@fontsource/open-sans/300.css"; /* light | normal */ +@import "../node_modules/@fontsource/open-sans/300-italic.css"; /* light | italic */ +@import "../node_modules/@fontsource/open-sans/400.css"; /* normal | normal */ +@import "../node_modules/@fontsource/open-sans/400-italic.css"; /* normal | italic */ +@import "../node_modules/@fontsource/open-sans/600.css"; /* heavier? | normal */ +@import "../node_modules/@fontsource/open-sans/600-italic.css"; /* heavier | italic */ +@import "../node_modules/@fontsource/open-sans/700.css"; /* bold | normal */ +@import "../node_modules/@fontsource/open-sans/700-italic.css"; /* bold | italic */ +@import "../node_modules/@fontsource/open-sans/800.css"; /* bolder? | normal */ +@import "../node_modules/@fontsource/open-sans/800-italic.css"; /* bolder? | italic */ + diff --git a/assets/css/_pygments.css b/assets/css/_pygments.css new file mode 100644 index 0000000..2931fbd --- /dev/null +++ b/assets/css/_pygments.css @@ -0,0 +1,269 @@ +pre { + line-height: 125%; +} +td.linenos .normal { + color: #37474f; + background-color: #263238; + padding-left: 5px; + padding-right: 5px; +} +span.linenos { + color: #37474f; + background-color: #263238; + padding-left: 5px; + padding-right: 5px; +} +td.linenos .special { + color: #607a86; + background-color: #263238; + padding-left: 5px; + padding-right: 5px; +} +span.linenos.special { + color: #607a86; + background-color: #263238; + padding-left: 5px; + padding-right: 5px; +} +pre.pygments .hll { + background-color: #2c3b41; +} +pre.pygments { + background: #263238; + color: #eeffff; +} +pre.pygments .c { + color: #546e7a; + font-style: italic; +} /* Comment */ +pre.pygments .err { + color: #ff5370; +} /* Error */ +pre.pygments .esc { + color: #89ddff; +} /* Escape */ +pre.pygments .g { + color: #eeffff; +} /* Generic */ +pre.pygments .k { + color: #bb80b3; +} /* Keyword */ +pre.pygments .l { + color: #c3e88d; +} /* Literal */ +pre.pygments .n { + color: #eeffff; +} /* Name */ +pre.pygments .o { + color: #89ddff; +} /* Operator */ +pre.pygments .p { + color: #89ddff; +} /* Punctuation */ +pre.pygments .ch { + color: #546e7a; + font-style: italic; +} /* Comment.Hashbang */ +pre.pygments .cm { + color: #546e7a; + font-style: italic; +} /* Comment.Multiline */ +pre.pygments .cp { + color: #546e7a; + font-style: italic; +} /* Comment.Preproc */ +pre.pygments .cpf { + color: #546e7a; + font-style: italic; +} /* Comment.PreprocFile */ +pre.pygments .c1 { + color: #546e7a; + font-style: italic; +} /* Comment.Single */ +pre.pygments .cs { + color: #546e7a; + font-style: italic; +} /* Comment.Special */ +pre.pygments .gd { + color: #ff5370; +} /* Generic.Deleted */ +pre.pygments .ge { + color: #89ddff; +} /* Generic.Emph */ +pre.pygments .gr { + color: #ff5370; +} /* Generic.Error */ +pre.pygments .gh { + color: #c3e88d; +} /* Generic.Heading */ +pre.pygments .gi { + color: #c3e88d; +} /* Generic.Inserted */ +pre.pygments .go { + color: #546e7a; +} /* Generic.Output */ +pre.pygments .gp { + color: #ffcb6b; +} /* Generic.Prompt */ +pre.pygments .gs { + color: #ff5370; +} /* Generic.Strong */ +pre.pygments .gu { + color: #89ddff; +} /* Generic.Subheading */ +pre.pygments .gt { + color: #ff5370; +} /* Generic.Traceback */ +pre.pygments .kc { + color: #89ddff; +} /* Keyword.Constant */ +pre.pygments .kd { + color: #bb80b3; +} /* Keyword.Declaration */ +pre.pygments .kn { + color: #89ddff; + font-style: italic; +} /* Keyword.Namespace */ +pre.pygments .kp { + color: #89ddff; +} /* Keyword.Pseudo */ +pre.pygments .kr { + color: #bb80b3; +} /* Keyword.Reserved */ +pre.pygments .kt { + color: #bb80b3; +} /* Keyword.Type */ +pre.pygments .ld { + color: #c3e88d; +} /* Literal.Date */ +pre.pygments .m { + color: #f78c6c; +} /* Literal.Number */ +pre.pygments .s { + color: #c3e88d; +} /* Literal.String */ +pre.pygments .na { + color: #bb80b3; +} /* Name.Attribute */ +pre.pygments .nb { + color: #82aaff; +} /* Name.Builtin */ +pre.pygments .nc { + color: #ffcb6b; +} /* Name.Class */ +pre.pygments .no { + color: #eeffff; +} /* Name.Constant */ +pre.pygments .nd { + color: #82aaff; +} /* Name.Decorator */ +pre.pygments .ni { + color: #89ddff; +} /* Name.Entity */ +pre.pygments .ne { + color: #ffcb6b; +} /* Name.Exception */ +pre.pygments .nf { + color: #82aaff; +} /* Name.Function */ +pre.pygments .nl { + color: #82aaff; +} /* Name.Label */ +pre.pygments .nn { + color: #ffcb6b; +} /* Name.Namespace */ +pre.pygments .nx { + color: #eeffff; +} /* Name.Other */ +pre.pygments .py { + color: #ffcb6b; +} /* Name.Property */ +pre.pygments .nt { + color: #ff5370; +} /* Name.Tag */ +pre.pygments .nv { + color: #89ddff; +} /* Name.Variable */ +pre.pygments .ow { + color: #89ddff; + font-style: italic; +} /* Operator.Word */ +pre.pygments .w { + color: #eeffff; +} /* Text.Whitespace */ +pre.pygments .mb { + color: #f78c6c; +} /* Literal.Number.Bin */ +pre.pygments .mf { + color: #f78c6c; +} /* Literal.Number.Float */ +pre.pygments .mh { + color: #f78c6c; +} /* Literal.Number.Hex */ +pre.pygments .mi { + color: #f78c6c; +} /* Literal.Number.Integer */ +pre.pygments .mo { + color: #f78c6c; +} /* Literal.Number.Oct */ +pre.pygments .sa { + color: #bb80b3; +} /* Literal.String.Affix */ +pre.pygments .sb { + color: #c3e88d; +} /* Literal.String.Backtick */ +pre.pygments .sc { + color: #c3e88d; +} /* Literal.String.Char */ +pre.pygments .dl { + color: #eeffff; +} /* Literal.String.Delimiter */ +pre.pygments .sd { + color: #546e7a; + font-style: italic; +} /* Literal.String.Doc */ +pre.pygments .s2 { + color: #c3e88d; +} /* Literal.String.Double */ +pre.pygments .se { + color: #eeffff; +} /* Literal.String.Escape */ +pre.pygments .sh { + color: #c3e88d; +} /* Literal.String.Heredoc */ +pre.pygments .si { + color: #89ddff; +} /* Literal.String.Interpol */ +pre.pygments .sx { + color: #c3e88d; +} /* Literal.String.Other */ +pre.pygments .sr { + color: #89ddff; +} /* Literal.String.Regex */ +pre.pygments .s1 { + color: #c3e88d; +} /* Literal.String.Single */ +pre.pygments .ss { + color: #89ddff; +} /* Literal.String.Symbol */ +pre.pygments .bp { + color: #89ddff; +} /* Name.Builtin.Pseudo */ +pre.pygments .fm { + color: #82aaff; +} /* Name.Function.Magic */ +pre.pygments .vc { + color: #89ddff; +} /* Name.Variable.Class */ +pre.pygments .vg { + color: #89ddff; +} /* Name.Variable.Global */ +pre.pygments .vi { + color: #89ddff; +} /* Name.Variable.Instance */ +pre.pygments .vm { + color: #82aaff; +} /* Name.Variable.Magic */ +pre.pygments .il { + color: #f78c6c; +} /* Literal.Number.Integer.Long */ diff --git a/assets/css/app.scss b/assets/css/app.scss index 54ad96b..7754710 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -16,6 +16,29 @@ /* Navbar custom styling */ @import "nav-bar-help"; +/* Pygments syntax highlighting styles */ +@import "pygments"; + +/* code highlighting */ +pre.pygments { + line-height: 125%; + background-color: #272822 !important; + overflow: wrap !important; + padding: 0.75rem; + border-radius: .25em; + code { + font-family: $font-family-code; + color: #eeffff; + } +} +code.inline { + background-color: #272822; + color: #eeffff; + padding: .2em .5em; + border-radius: .25em; + display: inline; +} + /* main */ html, body { @@ -24,12 +47,12 @@ body { height: 100%; } a { - color: $primary; + color: $secondary; &:visited { color: $info; } &:hover { - color: $secondary; + color: $primary; } } .border-gray-900 { @@ -59,15 +82,9 @@ a { .fw-600 { font-weight: 600; } -.font-sans-serif { - font-family: $font-family-sans-serif; -} .font-brand { font-family: $font-family-brand; } -.font-code { - font-family: $font-family-code; -} .fs-larger { font-size: larger; } @@ -124,10 +141,8 @@ a { margin-top: 2rem; } } -.post-lede.lead { - font-size: 1.25rem; - font-weight: 300; - color: $gray-300; +.post li { + line-height: 1.75rem; } /* extra */ diff --git a/assets/js/app.js b/assets/js/app.js index 3a2ff03..925e425 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -43,6 +43,9 @@ import { Socket } from "phoenix"; import topbar from "topbar"; import { LiveSocket } from "phoenix_live_view"; +// // Prismjs import +// import Prism from "prismjs" + // // Bootstrap v5 js imports // import "bootstrap/js/dist/alert"; import "bootstrap/js/dist/collapse"; diff --git a/assets/package-lock.json b/assets/package-lock.json index 6237e00..2d34020 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -7,17 +7,18 @@ "license": "MIT", "dependencies": { "@fontsource/fira-code": "^4.x", - "@fontsource/fira-mono": "^4.x", + "@fontsource/open-sans": "^4.2.2", "@fontsource/righteous": "^4.x", - "@fontsource/work-sans": "^4.2.2", "@mdi/svg": "^5.x", "@popperjs/core": "^2.x", + "babel-plugin-prismjs": "^2.0.1", "bootstrap": "^5.0.0-beta3", "bootstrap-icons": "^1.x", "hamburgers": "^1.x", "phoenix": "file:../deps/phoenix", "phoenix_html": "file:../deps/phoenix_html", "phoenix_live_view": "file:../deps/phoenix_live_view", + "prismjs": "^1.23.0", "topbar": "^1.x" }, "devDependencies": { @@ -1287,21 +1288,16 @@ "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-4.2.2.tgz", "integrity": "sha512-Bhg7rQ/CUbedA6B6K6gS2GDEa5JJjQwSqq1KMz4wVMaXXL+igsLrr4VKKmdfExwlB6o7Ie8kScXg4camZmt7TQ==" }, - "node_modules/@fontsource/fira-mono": { + "node_modules/@fontsource/open-sans": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-4.2.2.tgz", - "integrity": "sha512-t2WRThg+eLkQNQCtPG2sCCq40lz3xeb7nsL7P8l4+wfSRbdLQXAY5IebMftI2YEZR4MRRhdgrg0p5fi/2yXypA==" + "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.2.2.tgz", + "integrity": "sha512-NbsL1a9asJO6N/5kRxYPCy0kNhKMi9T75kl4QfIGtmpd/5IfB+UIAUxd9AICmCLaH4Osc2TImeTJj94xc9MNKg==" }, "node_modules/@fontsource/righteous": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@fontsource/righteous/-/righteous-4.2.2.tgz", "integrity": "sha512-mUjFblfCV6eWZj+lkrXFZsER8pq/3LOCoT3ezKPcerYH7StXQ8Gflcs0uMqacZP7CVLyzVUkPvSgLMQJTQvypg==" }, - "node_modules/@fontsource/work-sans": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@fontsource/work-sans/-/work-sans-4.2.2.tgz", - "integrity": "sha512-fFm8a1TbE+qDnRNsf4R6Z/yZP2f3mbj54zSoWUMIWVkgTHM0RfpcOTqoIp7Aj5jon6na0Oynlq5yXvMA5p3pKA==" - }, "node_modules/@mdi/svg": { "version": "5.9.55", "resolved": "https://registry.npmjs.org/@mdi/svg/-/svg-5.9.55.tgz", @@ -1876,6 +1872,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/babel-plugin-prismjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-prismjs/-/babel-plugin-prismjs-2.0.1.tgz", + "integrity": "sha512-GqQGa3xX3Z2ft97oDbGvEFoxD8nKqb3ZVszrOc5H7icnEUA56BIjVYm86hfZZA82uuHLwTIfCXbEKzKG1BzKzg==", + "peerDependencies": { + "prismjs": "^1.18.0" + } + }, "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -2315,6 +2319,17 @@ "node": ">=6" } }, + "node_modules/clipboard": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.8.tgz", + "integrity": "sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==", + "optional": true, + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -3133,6 +3148,12 @@ "node": ">=0.10.0" } }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "optional": true + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3946,6 +3967,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "optional": true, + "dependencies": { + "delegate": "^3.1.2" + } + }, "node_modules/graceful-fs": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", @@ -7588,6 +7618,14 @@ "posthtml-render": "^1.0.6" } }, + "node_modules/prismjs": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz", + "integrity": "sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==", + "optionalDependencies": { + "clipboard": "^2.0.0" + } + }, "node_modules/promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -8099,6 +8137,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=", + "optional": true + }, "node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -9164,6 +9208,12 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "optional": true + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -10914,21 +10964,16 @@ "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-4.2.2.tgz", "integrity": "sha512-Bhg7rQ/CUbedA6B6K6gS2GDEa5JJjQwSqq1KMz4wVMaXXL+igsLrr4VKKmdfExwlB6o7Ie8kScXg4camZmt7TQ==" }, - "@fontsource/fira-mono": { + "@fontsource/open-sans": { "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-4.2.2.tgz", - "integrity": "sha512-t2WRThg+eLkQNQCtPG2sCCq40lz3xeb7nsL7P8l4+wfSRbdLQXAY5IebMftI2YEZR4MRRhdgrg0p5fi/2yXypA==" + "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-4.2.2.tgz", + "integrity": "sha512-NbsL1a9asJO6N/5kRxYPCy0kNhKMi9T75kl4QfIGtmpd/5IfB+UIAUxd9AICmCLaH4Osc2TImeTJj94xc9MNKg==" }, "@fontsource/righteous": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@fontsource/righteous/-/righteous-4.2.2.tgz", "integrity": "sha512-mUjFblfCV6eWZj+lkrXFZsER8pq/3LOCoT3ezKPcerYH7StXQ8Gflcs0uMqacZP7CVLyzVUkPvSgLMQJTQvypg==" }, - "@fontsource/work-sans": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@fontsource/work-sans/-/work-sans-4.2.2.tgz", - "integrity": "sha512-fFm8a1TbE+qDnRNsf4R6Z/yZP2f3mbj54zSoWUMIWVkgTHM0RfpcOTqoIp7Aj5jon6na0Oynlq5yXvMA5p3pKA==" - }, "@mdi/svg": { "version": "5.9.55", "resolved": "https://registry.npmjs.org/@mdi/svg/-/svg-5.9.55.tgz", @@ -11394,6 +11439,12 @@ "@babel/helper-define-polyfill-provider": "^0.1.5" } }, + "babel-plugin-prismjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-prismjs/-/babel-plugin-prismjs-2.0.1.tgz", + "integrity": "sha512-GqQGa3xX3Z2ft97oDbGvEFoxD8nKqb3ZVszrOc5H7icnEUA56BIjVYm86hfZZA82uuHLwTIfCXbEKzKG1BzKzg==", + "requires": {} + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -11738,6 +11789,17 @@ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true }, + "clipboard": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.8.tgz", + "integrity": "sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==", + "optional": true, + "requires": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -12356,6 +12418,12 @@ "is-descriptor": "^1.0.0" } }, + "delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "optional": true + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -12979,6 +13047,15 @@ "slash": "^3.0.0" } }, + "good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "optional": true, + "requires": { + "delegate": "^3.1.2" + } + }, "graceful-fs": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", @@ -15706,6 +15783,14 @@ "posthtml-render": "^1.0.6" } }, + "prismjs": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz", + "integrity": "sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==", + "requires": { + "clipboard": "^2.0.0" + } + }, "promise-inflight": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", @@ -16072,6 +16157,12 @@ "ajv-keywords": "^3.5.2" } }, + "select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=", + "optional": true + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -16930,6 +17021,12 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "optional": true + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", diff --git a/assets/package.json b/assets/package.json index 98bc41b..f63b9f5 100644 --- a/assets/package.json +++ b/assets/package.json @@ -8,17 +8,18 @@ }, "dependencies": { "@fontsource/fira-code": "^4.x", - "@fontsource/fira-mono": "^4.x", + "@fontsource/open-sans": "^4.2.2", "@fontsource/righteous": "^4.x", - "@fontsource/work-sans": "^4.2.2", "@mdi/svg": "^5.x", "@popperjs/core": "^2.x", + "babel-plugin-prismjs": "^2.0.1", "bootstrap": "^5.0.0-beta3", "bootstrap-icons": "^1.x", "hamburgers": "^1.x", "phoenix": "file:../deps/phoenix", "phoenix_html": "file:../deps/phoenix_html", "phoenix_live_view": "file:../deps/phoenix_live_view", + "prismjs": "^1.23.0", "topbar": "^1.x" }, "devDependencies": { diff --git a/assets/webpack.config.js b/assets/webpack.config.js index 65d8d8a..1c82cd8 100644 --- a/assets/webpack.config.js +++ b/assets/webpack.config.js @@ -93,7 +93,7 @@ module.exports = (env, options) => { "../**/live/**/*.ex", "./js/**/*.js", ]), - safelist: [/phx/, /topbar/], + safelist: [/phx/, /topbar/, /linenos/, /pygments/], }), ] ), diff --git a/config/config.exs b/config/config.exs index 2b6d096..43a2dfd 100644 --- a/config/config.exs +++ b/config/config.exs @@ -11,7 +11,10 @@ use Mix.Config config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase # Custom application global variables -config :home73k, :app_global_vars, time_zone: "America/New_York" +config :home73k, :app_global_vars, + time_zone: "America/New_York", + blog_content: "priv/content", + pygmentize_bin: "priv/pygments/bin/pygmentize" # Configures the endpoint config :home73k, Home73kWeb.Endpoint, diff --git a/lib/home73k.ex b/lib/home73k.ex index 44aa497..b7c5af1 100644 --- a/lib/home73k.ex +++ b/lib/home73k.ex @@ -8,7 +8,9 @@ defmodule Home73k do """ @app_vars Application.compile_env(:home73k, :app_global_vars, time_zone: "America/New_York") - @app_time_zone @app_vars[:time_zone] - def app_time_zone, do: @app_time_zone + def app_vars, do: @app_vars + def app_time_zone, do: @app_vars[:time_zone] + def app_blog_content, do: @app_vars[:blog_content] + def app_pygmentize_bin, do: @app_vars[:pygmentize_bin] end diff --git a/lib/home73k/blog.ex b/lib/home73k/blog.ex index 806d291..7f002f6 100644 --- a/lib/home73k/blog.ex +++ b/lib/home73k/blog.ex @@ -3,7 +3,7 @@ defmodule Home73k.Blog do Application.ensure_all_started(:earmark) - posts_paths = "priv/content/**/*.md" |> Path.wildcard() + posts_paths = "#{Home73k.app_blog_content()}/**/*.md" |> Path.wildcard() posts = for post_path <- posts_paths do @@ -11,7 +11,7 @@ defmodule Home73k.Blog do Post.parse!(post_path) end - @posts Enum.sort_by(posts, & &1.date, {:desc, NaiveDateTime}) + @posts Enum.sort_by(posts, & &1.date, {:desc, Date}) @tags posts |> Stream.flat_map(& &1.tags) |> Stream.uniq() |> Enum.sort() diff --git a/lib/home73k/blog/post.ex b/lib/home73k/blog/post.ex index 1a5d1a6..c5ef717 100644 --- a/lib/home73k/blog/post.ex +++ b/lib/home73k/blog/post.ex @@ -1,4 +1,6 @@ defmodule Home73k.Blog.Post do + alias Home73k.Highlighter + @enforce_keys [:title, :id, :date, :author, :tags, :lede, :body, :corpus] defstruct [:title, :id, :date, :author, :tags, :lede, :body, :corpus] @@ -59,12 +61,10 @@ defmodule Home73k.Blog.Post do defp parse_lede(_), do: nil # """ parse_body/1 - # Convert body markdown to html - # TODO: handle syntax highlighting + # Convert body markdown to html, and highlight code fence blocks defp parse_body({fm, md}) do - Map.put(fm, :body, Earmark.as_html!(md)) - # TODO: Earmark.as_ast(md) |> parse_body(fm) - # def parse_body({:ok, ast, _}, fm) + html = Earmark.as_html!(md) |> Highlighter.highlight_code_blocks() + Map.put(fm, :body, html) end defp parse_body(_), do: nil @@ -128,8 +128,10 @@ defmodule Home73k.Blog.Post do # Handle split of post body. If lede found, return as html with body. # Otherwise return nil with body. # """ - defp extract_lede([lede, body]), - do: {String.trim_trailing(lede) |> Earmark.as_html!(), String.trim_leading(body)} + defp extract_lede([lede, body]) do + lede_html = String.trim_trailing(lede) |> Earmark.as_html!() |> Highlighter.highlight_code_blocks() + {lede_html, String.trim_leading(body)} + end defp extract_lede([body]), do: {nil, body} diff --git a/lib/home73k/highlighter.ex b/lib/home73k/highlighter.ex new file mode 100644 index 0000000..b76ff18 --- /dev/null +++ b/lib/home73k/highlighter.ex @@ -0,0 +1,52 @@ +defmodule Home73k.Highlighter do + @moduledoc """ + Performs code highlighting. + """ + + alias Home73k.Temp + + @pygments_cmd Home73k.app_pygmentize_bin() |> Path.expand() + + @doc """ + Highlights all code block in an already generated HTML document. + """ + def highlight_code_blocks(html) do + ~r/
([^<]*)<\/code><\/pre>/
+    |> Regex.replace(html, &highlight_code_block(&1, &2, &3))
+  end
+
+  defp highlight_code_block(_full_block, lang, code) do
+    # unescape the code
+    unescaped_code = unescape_html(code) |> IO.iodata_to_binary()
+
+    # write code to temp file
+    tmp_file = Temp.file()
+    File.write!(tmp_file, unescaped_code)
+
+    # pygmentize the code via temp file
+    pyg_args = ["-l", lang, "-f", "html", "-O", "cssclass=pygments", tmp_file]
+    {highlighted, _} = System.cmd(@pygments_cmd, pyg_args)
+
+    # correct pygment wrapping markup
+    highlighted
+    |> String.replace("", "")
+    |> String.replace("
", "
")
+    |> String.replace("
", "
") + end + + entities = [{"&", ?&}, {"<", ?<}, {">", ?>}, {""", ?"}, {"'", ?'}] + + for {encoded, decoded} <- entities do + defp unescape_html(unquote(encoded) <> rest) do + [unquote(decoded) | unescape_html(rest)] + end + end + + defp unescape_html(<>) do + [c | unescape_html(rest)] + end + + defp unescape_html(<<>>) do + [] + end +end diff --git a/lib/home73k/temp.ex b/lib/home73k/temp.ex new file mode 100644 index 0000000..08f78fe --- /dev/null +++ b/lib/home73k/temp.ex @@ -0,0 +1,19 @@ +defmodule Home73k.Temp do + @moduledoc """ + Simple module to generate temporary files + """ + def file do + System.tmp_dir!() + |> Path.join(random_filename()) + |> touch_file() + end + + defp random_filename do + :crypto.strong_rand_bytes(32) |> Base.url_encode64 |> binary_part(0, 32) + end + + defp touch_file(fdname) do + File.touch!(fdname) + fdname + end +end diff --git a/lib/home73k_web.ex b/lib/home73k_web.ex index 29f5403..6da50d5 100644 --- a/lib/home73k_web.ex +++ b/lib/home73k_web.ex @@ -93,6 +93,9 @@ defmodule Home73kWeb do # Import SVG Icon helper import Home73kWeb.IconHelpers + # Import Date formatter helper + import Home73kWeb.DateHelpers + import Home73kWeb.ErrorHelpers import Home73kWeb.Gettext alias Home73kWeb.Router.Helpers, as: Routes diff --git a/lib/home73k_web/live/blog_live.ex b/lib/home73k_web/live/blog_live.ex index fb8a99c..6100d73 100644 --- a/lib/home73k_web/live/blog_live.ex +++ b/lib/home73k_web/live/blog_live.ex @@ -48,7 +48,4 @@ defmodule Home73kWeb.BlogLive do # do: {app, vsn} # end - defp format_date(date) do - Calendar.strftime(date, "%B %-d, %Y") - end end diff --git a/lib/home73k_web/live/blog_live.html.leex b/lib/home73k_web/live/blog_live.html.leex index 1e055c9..5dd8a55 100644 --- a/lib/home73k_web/live/blog_live.html.leex +++ b/lib/home73k_web/live/blog_live.html.leex @@ -1,22 +1,22 @@
-
+
<%= for post <- @posts do %>
-

+

<%= live_redirect "#{post.title}", to: Routes.post_path(@socket, :show, post) %>

-