A Python-first compiler for building beautiful static websites.
Project description
ARKlight
A Python-first compiler for building beautiful static websites.
Users write Python. ARKlight produces standard HTML. The browser never executes Python.
from arklight import *
site = Site()
@site.page("/")
def home():
return Page(
Heading("ARKlight"),
Text("Build websites with Python."),
Button("Get Started"),
)
arklight build site.py -o ARK
produces ARK/index.html -- plain, dependency-free HTML.
Status
Current release: v0.041 -- CLI, pipeline & JS runtime hardening.
main() now wraps subcommand dispatch in a catch-all so unhandled
errors print a clear message and exit 1 instead of a raw traceback;
build()'s file writes/asset copy are guarded against filesystem
failures; and the generated arklight.js runtime gained an
arkNotify() on-page notice plus try/catch guards throughout, so
one bad element or a clipboard failure can't take the rest of a page's
interactivity down with it. This release also folds in the stateful-JS
vocabulary addenda (Action.decrement, Action.reset,
Action.append, Action.remove). Full detail in
CHANGELOG.md; narrative/decision log in
PROGRESS.md.
Next up: v0.048 -- CSS @media queries + <head>/<header>
extension. Design complete, implementation not started. See
docs/DESIGN-NOTES.md ("v0.048: CSS media
queries + <head> extension"). Custom CSS class authoring and an
arklight --search <name> schema lookup are sketched but not yet
scheduled to a version.
See docs/ARCHITECTURE.md for the full
milestone roadmap.
Install
pip install -e .
This installs the arklight package and the arklight CLI command
(defined in pyproject.toml).
CLI
arklight build <entry.py> [-o OUTPUT_DIR] [--open | --no-open]
entry.py-- your site file (must definesite = Site()and at least one@site.page("/route")-decorated function).-o, --output-- output directory, defaultARK/.- If a top-level
assets/folder sits next toentry.py, it is copied (recursively) into<output>/assetsautomatically. --open(default) -- opensindex.htmlin your default browser after building.--no-opendisables this.
Try the bundled example -- this builds the site AND opens it in your browser:
arklight build examples/hello_site/site.py -o ARK
arklight pack <build-dir> [-o OUTPUT.ark] [--plain] [--passphrase PASSPHRASE]
build-dir-- an existingarklight buildoutput directory (e.g.ARK).-o, --output-- output bundle path, defaultsite.ark.- Packs the build directory into a single
.arkfile: an HTML/archive polyglot (see "ARK Bundle" below), carrying over every file inbuild-dirincludingassets/. - Sealed by default -- the archive half is encrypted, opaque to
generic archive tools.
--passphrase PASSPHRASEderives the key from a passphrase instead of an embedded one, for real confidentiality (the same passphrase is then required to unpack).--plainskips sealing entirely and produces a plain, freely-openable ZIP tail (the original v1 behavior).
arklight unpack <bundle.ark> [-o OUTPUT_DIR] [--passphrase PASSPHRASE]
bundle.ark-- a.arkfile produced byarklight pack.-o, --output-- output directory, defaultARK.- Extracts the archive half back into a normal build directory.
Auto-detects sealed vs. plain bundles;
--passphraseis only needed if the bundle was sealed with one.
arklight build examples/hello_site/site.py -o ARK --no-open
arklight pack ARK -o hello_site.ark
arklight unpack hello_site.ark -o restored
Compiler pipeline
ARKlight compiles a site in clearly separated stages, each in its own part of the package:
Python Source
|
v
Python AST arklight/parser/discover.py
| (static analysis via the stdlib `ast` module:
| finds Site()/@site.page(...) without executing
| user code)
v
ARK AST arklight/parser/loader.py + arklight/api.py
| (the module is executed; calling Heading(...),
| Text(...), etc. builds a tree of ARKNode objects
| -- that tree IS the ARK AST)
v
Normalization arklight/ir/normalize.py
| (flattens nested lists, drops None/False,
| wraps bare strings as Text nodes where needed)
v
Validation arklight/ir/validate.py
| (schema check: known component types, required
| props, valid text-only nesting)
v
Website IR arklight/ir/build.py
| (backend-independent IRNode tree: type/props/children
| -- models website *intent*, not HTML)
v
Backend Interface arklight/backend/base.py
| (abstract `Backend.render(ir) -> {path: contents}`)
v
HTML Backend arklight/backend/html/render.py
| (maps IR node types to HTML tags, rewrites internal
| Link/Image hrefs to relative file paths, links the
| generated stylesheet and behavior runtime)
v
CSS Backend arklight/backend/css/render.py
| (v0.002: generates a global default stylesheet)
v
JS Backend arklight/backend/js/render.py
| (v0.003: generates a tiny fixed behavior runtime;
| all three backends run over the same IR and their
| outputs are merged)
v
index.html, about.html, styles.css, arklight.js, ...
arklight/compiler/pipeline.py orchestrates all of the above into a
single build(entry_path, output_dir) call, which is what the CLI
uses. By default it runs [HTMLBackend(), CSSBackend(), JSBackend()]
-- pass your own backends=[...] list to customize which backends run.
Internal links are relative, not root-absolute
Link("About", href="/about") refers to the route "/about", the
same string you'd pass to @site.page(...). The HTML backend resolves
this to the correct relative file path at build time (about.html,
../about.html, etc., depending on where the linking page lives), so
navigation works whether you open the file directly from disk or
deploy the ARK/ folder as-is. External URLs, #fragments, and
mailto:/tel: links are left untouched.
Styling components
Any component accepts two extra props for styling, on top of the default stylesheet:
Text("Careful now", class_name="muted")
Container(..., style={"background": "#f5f5ff", "padding": "1rem"})
class_name renders as the HTML class attribute (avoiding the
class keyword clash); style accepts a dict of CSS properties and
is rendered as an inline style attribute. Built-in utility classes
from the default stylesheet: .nav, .card, .muted, .page,
.hidden (pairs with the toggle behavior below).
Responsive layout, without @media (platform-independent by construction)
Page never gets a <head> hook (see
docs/DESIGN-NOTES.md), so a generated site
has no @media/@container query available to it at all -- there is
no "desktop breakpoint" or "mobile breakpoint" to hand-tune, and
nothing keyed to a specific screen width, device, or platform. Layouts
still adapt, but from the content's own available width using plain
flexbox/grid sizing keywords (minmax, auto-fit, flex-wrap,
clamp) -- the same technique goes by "intrinsic web design." Opt in
with class_name, same mechanism as .nav/.card above:
| Class | What it does |
|---|---|
.stack |
Consistent vertical rhythm between block children |
.cluster |
A row of items that wraps as a group once it runs out of width |
.sidebar |
Two panels side-by-side once there's room, stacked when there isn't |
.switcher |
Children stay in a row until each would drop below a minimum width, then stack |
.grid |
An auto-filling card/tile grid with no explicit column count |
.center |
Constrains and horizontally centers content, with optional gutters |
.reel |
A horizontally-scrolling row that never wraps or overflows the page |
.fluid-heading |
Font size scales smoothly with available width via clamp() |
For a layout that's three columns on a wide viewport and a single
column on a narrow one -- the classic "desktop vs. mobile" case --
.switcher is usually the right tool: give it three children and it
lays them out in a row as long as each stays above a minimum width
(--ark-switcher-threshold, default 30rem), and stacks them
vertically the moment they'd drop below that, with no device or
browser ever queried:
Container(
Container(Heading("Fast"), Text("...", class_name="muted"), class_name="card"),
Container(Heading("Simple"), Text("...", class_name="muted"), class_name="card"),
Container(Heading("Portable"), Text("...", class_name="muted"), class_name="card"),
class_name="switcher",
)
This adapts identically on any platform that renders CSS at all --
desktop browser, phone browser, embedded webview -- because the
decision is made from the container's own measured width, not from
user-agent, viewport metadata, or a hard-coded pixel breakpoint.
.grid is the equivalent choice when the number of items is open-ended
rather than a fixed three (a card feed, a tag list), auto-filling as
many minmax()-wide columns as the available width allows. All
--ark-* custom properties above (space, thresholds, widths) can be
overridden per-instance via the style prop shown earlier.
Behaviors (client-side interactivity, no JS written by hand)
Any component accepts on_click + behavior_target (a CSS selector)
to opt into a small, closed set of built-in behaviors, implemented by
the tiny runtime the JS backend generates:
Button(
"Show details",
on_click="toggle", # or "scroll-to"
behavior_target="#more-details",
toggle_class="hidden", # optional, default "is-open"
)
Container(Text("..."), id="more-details", class_name="hidden")
| Behavior | What it does |
|---|---|
toggle |
Toggles a CSS class (toggle_class, default is-open) on every element matching behavior_target |
scroll-to |
Smooth-scrolls the element matching behavior_target into view |
on_click is validated against this fixed vocabulary at the
Validation stage -- an unknown behavior name (or a missing
behavior_target) fails the build with a clear message rather than
silently doing nothing in the browser. There is deliberately no way to
pass arbitrary JavaScript: see
docs/DESIGN-NOTES.md for why that boundary
is a design choice, not a gap.
The current page's nav link is also highlighted automatically (an
is-active class added to any <a> inside .nav whose target matches
the current page) -- no props needed for that one.
Public API (v0.003)
Components -- every one of these is a plain Python function that
returns an ARKNode:
| Component | Notes |
|---|---|
Page |
Root node every page function must return |
Container |
Generic grouping element (renders as <div>) |
Heading |
Text-only. level=1..6 prop controls <h1>-<h6> |
Text |
Text-only. Renders as <p> |
Button |
Text-only. Renders as <button> |
Link |
Text-only. Requires href prop. Renders as <a> |
Image |
No children allowed. Requires src prop |
List |
Renders as <ul> |
Item |
Text-only. Renders as <li> |
"Text-only" components may only contain plain strings, not other components -- this is enforced by the Validation stage.
The table above is the original v0.001 core. Two vocabulary
addenda (still v0.003, no new pipeline stage -- see CHANGELOG.md) add
~79 more components on top of it, purely as data in
arklight.ir.schema.SCHEMA (the single source of truth every stage
reads from):
- First addendum: semantic layout (
Header,Footer,Main,Nav,Section,Article,Aside,Figure/FigCaption,Details/Summary), text-level semantics (Strong,Em,Small,Mark,Code,Cite,Abbr,Sub,Sup,Span,Time,HorizontalRule,LineBreak,Pre,Blockquote), forms (Form,Input,Textarea,Select,Option,OptGroup,Label,FieldSet,Legend), tables (Table,TableHead,TableBody,TableFoot,TableRow,TableHeaderCell,TableCell,Caption), and media (Video,Audio,Source). - Second addendum ("even more vocabulary"): numbered/description
lists (
OrderedList,DescriptionList/DescriptionTerm/DescriptionDetails), art-directed responsive images (Picture/PictureSource, plusloading/decodingattributes), native widgets (Progress,Meter,Datalist,Output), a zero-JSDialog, more text semantics including bidi and ruby (Kbd,Samp,Var,Data,Ins,Del,Q,Dfn,Address,Wbr,Bdi,Bdo,Ruby,Rt,Rp), table column grouping (ColGroup,Col), video/audio captions (Track), image maps (Map,Area),IFrameembeds, and aNoScriptfallback.
See CHANGELOG.md for the rationale behind each
group and arklight.ir.schema.SCHEMA for the authoritative list of
every component's required props, text-only-children rule, and
whether it allows children at all.
Site:
site = Site()
@site.page("/some/route")
def page_fn():
return Page(...)
Any keyword prop passed to a component that isn't recognized (e.g.
id, class, href, src, style, ...) is emitted as a data-*
HTML attribute, so nothing you write is silently dropped.
ARK Bundle (.ark) -- v0.037 (sealed by default, implemented)
A build's output (index.html, styles.css, arklight.js, assets/)
is a folder of separate files. arklight pack (see CLI section above)
packages that output as a single .ark file:
- The raw build files -- including an
assets/folder, if present -- are stored as-is inside an archive; no new file format, no re-encoding, nothing about how the HTML/CSS/JS backends generate files changes for this feature. - The bundle is a polyglot: a fully self-contained, inlined
rendering of the entry page is placed before the archive data, so
the same file opens directly as a rendered page in a browser (no
unzip step, no temp files, no server -- the same way an image viewer
doesn't "extract" a
.pngbefore displaying it) regardless of what the archive half contains. - Sealed by default. The archive half is encrypted (see
arklight.packer.seal, stdlibhmac/hashlib/secretsonly, no crypto dependency) so a generic archive tool, "rename to.zip", or hex editor sees only opaque bytes -- it can't be casually opened, inspected, or spliced/tampered with.arklight unpack site.ark -o ARKreverses this. Without a--passphrase, the encryption key travels embedded in the bundle soarklight unpackalways works with no extra input -- this blocks generic tools, but is not secrecy from someone who also has ARKlight (the key is right there in the file). Pass--passphrasefor real confidentiality: the key is then derived from it (PBKDF2-HMAC-SHA256) and never stored, and the same passphrase is required to unpack. --plainopts back into the original v1 behavior: a real, generically-openable ZIP tail, freely inspectable/re-editable by any archive tool without ARKlight installed at all.- This is a packaging step that runs after
arklight build, over files the existing pipeline already produces --arklight.packeronly reads already-written build output and never imports the parser/ir/backend internals. - Known limit, sealed or not: only the archive half is protected. The inlined front-matter page -- what a browser actually renders -- is always plain HTML/CSS/JS, because that's what makes the polyglot openable as a web page at all; view-source on the page you're currently looking at was never in scope to hide. Sealing protects the other pages/assets bundled alongside it, not the one on screen.
See docs/DESIGN-NOTES.md ("v0.036: ARK
Bundle spec v1" and "v0.037: sealed bundles") for the full byte layout,
packing algorithm, cipher construction, and known caveats.
Repository layout
arklight-framework/
arklight/
api.py Public component functions + Site class
ast/ ARK AST node type (ARKNode)
parser/ Python Source -> Python AST -> (loaded) ARK AST
ir/ Normalization, Validation, Website IR
backend/
base.py Backend interface
html/ HTML backend (the only backend in v0.001)
js/
behaviors/ v0.0035 behavior fragments (toggle, scroll-to,
copy, dismiss) -- one file per
BEHAVIOR_REGISTRY entry
actions/ v0.0035 action fragments (set, increment,
toggle_bool) -- one file per ACTION_REGISTRY
entry
compiler/ Pipeline orchestration
cli/ `arklight` command-line entry point
templates/ `simple`/`production` scaffolds for
`arklight new` (v0.004a; see docs/DESIGN-NOTES.md)
packer/ `arklight pack` -- ARK Bundle (.ark) packaging,
reads already-built output only, never touches
the compiler pipeline
examples/
hello_site/ Example site matching this README
tests/ Unit + end-to-end tests for every pipeline stage
docs/ Additional design notes
PROGRESS.md What's done, what's next
CHANGELOG.md Version history
Running tests
pip install pytest
pytest
Non-goals (v0.001 and for the foreseeable future)
- Browser-side Python
- Virtual DOM
- Runtime Python execution in the browser
- Feature creep beyond the milestone roadmap below
Roadmap
Full milestone table (with status) lives in
docs/ARCHITECTURE.md -- kept as the single
canonical copy rather than duplicated here, in PROGRESS.md, and in
CHANGELOG.md. Short version: v0.001 through v0.041 are done; v0.048
(CSS @media + <head>/<header> extension) is next; v0.010
(components) and v0.100 (alternate backends) are further out.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file arklight-0.42.0.tar.gz.
File metadata
- Download URL: arklight-0.42.0.tar.gz
- Upload date:
- Size: 108.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f5da9eb59a0f350c94c6dc36cc07b8df763b38091efb7c6dcb1773b10f60efd
|
|
| MD5 |
1706e33de96c4a4ed4711439ee7e23d1
|
|
| BLAKE2b-256 |
9c2483ec5b62ccc74e557d38411a30bb1408df85d509d6f5fb5992df3c4b12fc
|
Provenance
The following attestation bundles were made for arklight-0.42.0.tar.gz:
Publisher:
publish.yml on Rae-ARK/ARKlight
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arklight-0.42.0.tar.gz -
Subject digest:
5f5da9eb59a0f350c94c6dc36cc07b8df763b38091efb7c6dcb1773b10f60efd - Sigstore transparency entry: 2337031053
- Sigstore integration time:
-
Permalink:
Rae-ARK/ARKlight@cab0892189bc05d107b322e2c14ab1e0f3cffc1a -
Branch / Tag:
refs/tags/v0.42.0 - Owner: https://github.com/Rae-ARK
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cab0892189bc05d107b322e2c14ab1e0f3cffc1a -
Trigger Event:
release
-
Statement type:
File details
Details for the file arklight-0.42.0-py3-none-any.whl.
File metadata
- Download URL: arklight-0.42.0-py3-none-any.whl
- Upload date:
- Size: 96.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97b7f98f1238a62b27855d1aaf3004aa9b5abd18be72e8f175a2316da32eb757
|
|
| MD5 |
8575cec7f49c1290f40f3ea0c75e5c20
|
|
| BLAKE2b-256 |
a5ded2857d77f2b35dd2d371634d12bb939e469ac3c4c693a5b153257562bd5c
|
Provenance
The following attestation bundles were made for arklight-0.42.0-py3-none-any.whl:
Publisher:
publish.yml on Rae-ARK/ARKlight
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
arklight-0.42.0-py3-none-any.whl -
Subject digest:
97b7f98f1238a62b27855d1aaf3004aa9b5abd18be72e8f175a2316da32eb757 - Sigstore transparency entry: 2337031165
- Sigstore integration time:
-
Permalink:
Rae-ARK/ARKlight@cab0892189bc05d107b322e2c14ab1e0f3cffc1a -
Branch / Tag:
refs/tags/v0.42.0 - Owner: https://github.com/Rae-ARK
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cab0892189bc05d107b322e2c14ab1e0f3cffc1a -
Trigger Event:
release
-
Statement type: