Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
33 changes: 31 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
name: build
on:
pull_request:
branches:
- "main"

jobs:
# Existing Jekyll build job – kept intact during the migration parallel phase.
build:
runs-on: ubuntu-latest

Expand Down Expand Up @@ -42,3 +41,33 @@ jobs:
./scripts/build
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Laika/Scala build validation job – added as part of migration workstream W0.5.
# Runs on every PR alongside the Jekyll job; does NOT deploy output.
laika-build:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Cache Scala-CLI build artifacts
uses: actions/cache@v4
with:
path: ~/.cache/scala-cli
key: scala-cli-${{ runner.os }}-${{ hashFiles('build.scala', 'src/**/*.scala') }}
restore-keys: |
scala-cli-${{ runner.os }}-

- name: Install Scala CLI
uses: VirtusLab/scala-cli-setup@v1

- name: Check build
run: |
scala-cli compile --server=false ./build.scala

- name: Laika build (validation only)
run: |
scala-cli run --server=false build.scala -- build --out _site-laika
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
_site
_site-laika
_site-jekyll
.sass-cache
.DS_Store
.ruby-version
Expand Down
17 changes: 17 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version = 3.10.0

runner.dialect = scala3
runner.dialectOverride.allowSignificantIndentation = false
runner.dialectOverride.allowQuietSyntax = true

maxColumn = 100
indent.main = 2
indent.callSite = 2
indent.extendSite = 2

newlines.source = keep
rewrite.scala3.convertToNewSyntax = true
rewrite.scala3.removeOptionalBraces = false

rewrite.rules = [Imports]
rewrite.imports.sort = scalastyle
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ Use `plans/tech-conversion-to-laika.md` for migration scope and sequencing.

## Scala and Build Rules

- Build tool is `Scala-CLI` only. Do not add `sbt` files.
- Build tool is `Scala-CLI` only (available as standalone script as `./scala`). Do not add `sbt` files.
- Use Scala 3.
- Required scalac options: `--no-indent --rewrite`.
- Keep `build.scala` at repo root as a thin entrypoint.
- To keep Scala dependencies up to date:
`./scala --power dependency-update ./build.scala --all`
- Verify Scala code compiles by running:
`./scala compile ./build.scala`.

## Project Organization Rules

Expand Down
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,19 @@ build: hooks

hooks: ./.git/hooks/pre-commit

scala-edit:
rm -rf .idea/ .bsp .metals .scala-build
./scala setup-ide ./build.scala
code . --goto ./build.scala

scala-compile:
./scala compile ./build.scala

scala-build:
./scala ./build.scala -- build --out _site-laika

scala-update-dependencies:
./scala --power dependency-update ./build.scala --all

./.git/hooks/pre-commit:
./scripts/install-hooks
27 changes: 27 additions & 0 deletions build.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//> using scala "3.8.2"
//> using options "-no-indent" "-rewrite"
//> using files "./src/"
//> using dep "org.typelevel::laika-io:1.3.2"
//> using dep "org.typelevel::cats-effect:3.6.3"
//> using dep "com.monovore::decline-effect:2.6.0"
//> using dep "org.slf4j:slf4j-nop:2.0.17"

import com.monovore.decline.effect.CommandIOApp
import com.monovore.decline.{Command, Opts}
import cats.effect.{IO, ExitCode}

/** Thin Scala-CLI command entrypoint for the Laika build.
*
* Run with:
* ./scala build.scala -- build [--out <dir>]
* ./scala build.scala -- serve [--port <port>]
* ./scala build.scala -- verify
*/
object Main extends CommandIOApp(
name = "build",
header = "alexn.org site build tool (Laika)"
) {
override def main: Opts[IO[ExitCode]] = {
Build.command
}
}
65 changes: 65 additions & 0 deletions scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash

# This is the launcher script of Scala CLI (https://github.com/VirtusLab/scala-cli).
# This script downloads and runs the Scala CLI version set by SCALA_CLI_VERSION below.
#
# Download the latest version of this script at https://github.com/VirtusLab/scala-cli/raw/main/scala-cli.sh

set -eu

SCALA_CLI_VERSION="1.12.4"

GH_ORG="VirtusLab"
GH_NAME="scala-cli"

if [ "$SCALA_CLI_VERSION" == "nightly" ]; then
TAG="nightly"
else
TAG="v$SCALA_CLI_VERSION"
fi

if [ "$(expr substr $(uname -s) 1 5 2>/dev/null)" == "Linux" ]; then
arch=$(uname -m)
if [[ "$arch" == "aarch64" ]] || [[ "$arch" == "x86_64" ]]; then
SCALA_CLI_URL="https://github.com/$GH_ORG/$GH_NAME/releases/download/$TAG/scala-cli-${arch}-pc-linux.gz"
else
echoerr "scala-cli is not supported on $arch"
exit 2
fi
CACHE_BASE="$HOME/.cache/coursier/v1"
elif [ "$(uname)" == "Darwin" ]; then
arch=$(uname -m)
CACHE_BASE="$HOME/Library/Caches/Coursier/v1"
if [[ "$arch" == "x86_64" ]]; then
SCALA_CLI_URL="https://github.com/$GH_ORG/$GH_NAME/releases/download/$TAG/scala-cli-x86_64-apple-darwin.gz"
elif [[ "$arch" == "arm64" ]]; then
SCALA_CLI_URL="https://github.com/$GH_ORG/$GH_NAME/releases/download/$TAG/scala-cli-aarch64-apple-darwin.gz"
else
echoerr "scala-cli is not supported on $arch"
exit 2
fi
else
echo "This standalone scala-cli launcher is supported only in Linux and macOS. If you are using Windows, please use the dedicated launcher scala-cli.bat"
exit 1
fi

CACHE_DEST="$CACHE_BASE/$(echo "$SCALA_CLI_URL" | sed 's@://@/@')"
SCALA_CLI_BIN_PATH=${CACHE_DEST%.gz}

if [ ! -f "$CACHE_DEST" ]; then
mkdir -p "$(dirname "$CACHE_DEST")"
TMP_DEST="$CACHE_DEST.tmp-setup"
echo "Downloading $SCALA_CLI_URL"
curl -fLo "$TMP_DEST" "$SCALA_CLI_URL"
mv "$TMP_DEST" "$CACHE_DEST"
fi

if [ ! -f "$SCALA_CLI_BIN_PATH" ]; then
gunzip -k "$CACHE_DEST"
fi

if [ ! -x "$SCALA_CLI_BIN_PATH" ]; then
chmod +x "$SCALA_CLI_BIN_PATH"
fi

exec "$SCALA_CLI_BIN_PATH" "$@"
123 changes: 123 additions & 0 deletions src/Build.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import cats.effect.{IO, ExitCode, Resource}
import cats.syntax.all.*
import com.monovore.decline.{Command, Opts}
import laika.api.Transformer
import laika.format.{Markdown, HTML}
import laika.io.syntax.*
import laika.io.model.{InputTree, InputTreeBuilder}
import laika.io.api.TreeTransformer
import laika.ast.Path.Root
import extensions.CompatBundle

/** Main build pipeline orchestration.
*
* Subcommands (wired via Decline):
* build -- render all content to _site-laika/ (or --out <dir>)
* serve -- start a local preview server (--port <n>, default 4000)
* verify -- compare selected Laika output against Jekyll baseline
*/
object Build {

// ---------------------------------------------------------------------------
// CLI model (Decline)

private val buildCmd: Command[IO[ExitCode]] = {
val outDir = Opts
.option[String]("out", help = "Output directory (default: _site-laika)", short = "o")
.withDefault("_site-laika")
Command("build", "Render the site to an output directory") {
outDir.map(runBuild)
}
}

private val serveCmd: Command[IO[ExitCode]] = {
val port = Opts
.option[Int]("port", help = "Port for the preview server (default: 4000)", short = "p")
.withDefault(4000)
Command("serve", "Start a local preview server") {
port.map(runServe)
}
}

private val verifyCmd: Command[IO[ExitCode]] = {
Command("verify", "Compare Laika output against Jekyll baseline") {
Opts(runVerify())
}
}

/** Top-level Decline command exposed to `Main` in `build.scala`. */
val command: Opts[IO[ExitCode]] = {
Opts.subcommands(buildCmd, serveCmd, verifyCmd)
}

// ---------------------------------------------------------------------------
// Transformer

/** Laika transformer with GitHub-flavoured Markdown and the site-specific
* compatibility bundle. Returned as a `Resource` so that the underlying
* thread pool is properly released on completion or error.
*/
def transformerResource: Resource[IO, TreeTransformer[IO]] = {
Transformer
.from(Markdown)
.to(HTML)
.using(Markdown.GitHubFlavor, CompatBundle)
.parallel[IO]
.build
}

// ---------------------------------------------------------------------------
// Input tree

/** Builds the virtual input tree by merging:
* - Static passthrough assets (assets/, robots.txt, manifest.webmanifest,
* crossdomain.xml, 404.html, CNAME)
* - Standalone pages under docs/
*
* Blog posts (_posts/) and wiki pages (_wiki/) will be wired in W2 once
* the permalink / front-matter compatibility layer is in place.
*/
def inputTree: InputTreeBuilder[IO] = {
InputTree[IO]
// Static assets: preserve the assets/ prefix in the output tree
.addDirectory("assets", Root / "assets")
// Passthrough files expected at the root of the generated site
.addFile("robots.txt", Root / "robots.txt")
.addFile("manifest.webmanifest", Root / "manifest.webmanifest")
.addFile("crossdomain.xml", Root / "crossdomain.xml")
.addFile("404.html", Root / "404.html")
.addFile("CNAME", Root / "CNAME")
// Static HTML/markdown pages under docs/
.addDirectory("docs", Root / "docs")
}

// ---------------------------------------------------------------------------
// Commands

def runBuild(outDir: String): IO[ExitCode] = {
transformerResource.use { transformer =>
transformer
.fromInput(inputTree)
.toDirectory(outDir)
.transform
.as(ExitCode.Success)
}
}

def runServe(port: Int): IO[ExitCode] = {
// Preview server via laika.preview.ServerBuilder will be wired in a
// later workstream once the full input tree is stable.
IO.println(
s"Preview server on port $port is not yet implemented. " +
"Use 'bundle exec jekyll serve' in the meantime."
) *> IO.pure(ExitCode.Error)
}

def runVerify(): IO[ExitCode] = {
// Parity comparison against Jekyll baseline will be implemented in W11.
IO.println(
"Verify: side-by-side Jekyll vs Laika comparison not yet implemented."
) *> IO.pure(ExitCode.Success)
}
}

28 changes: 28 additions & 0 deletions src/SiteConfig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/** Site-wide metadata, mirroring the values from Jekyll's _config.yml. */
object SiteConfig {

val title: String = "Alexandru Nedelcu"
val description: String = "On programming and personal projects"
val domain: String = "alexn.org"
val url: String = "https://alexn.org"
val baseUrl: String = ""

object Author {
val name: String = "Alexandru Nedelcu"
val github: String = "alexandru"
val linkedin: String = "alexelcu"
val mastodon: String = "https://mastodon.social/@alexelcu"
val bluesky: String = "https://bsky.app/profile/alexn.org"
}

object RepoEdit {
val base: String = "https://github.com/alexandru/alexn.org/blob/main/"
}

val navigation: List[(String, String)] = List(
"Blog" -> "/blog/",
"Wiki" -> "/wiki/",
"About" -> "/about/",
"Subscribe" -> "/subscribe/"
)
}
23 changes: 23 additions & 0 deletions src/extensions/CompatBundle.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package extensions

import laika.api.bundle.ExtensionBundle

/** Compatibility extension bundle for the Jekyll → Laika migration.
*
* During the dual-run phase (W1–W11) this bundle provides:
* - YAML front-matter extraction and mapping to Laika config values.
* - Compatibility interpretation of Liquid constructs kept in source
* content ({% link %}, {% post_url %}, {% include youtube.html %},
* {% raw %}…{% endraw %}).
* - AST rewrite hooks (fence header normalisation, math markers, etc.).
* - Renderer overrides for output-specific behaviour.
* - Custom directives (YouTube embed, contribution/subscription blocks).
*
* New workstream implementations (W2-W12) will be added here
* incrementally without changing the public API surface.
*/
object CompatBundle extends ExtensionBundle {

override val description: String =
"alexn.org Jekyll compatibility bundle"
}