Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 105e68f036 | |||
| 792878d6f3 | |||
| 910619bc7a | |||
| e0e4096f93 | |||
| c176719031 | |||
| a1b82ebd1b | |||
| 5ad8fff9df | |||
| 7157144c75 | |||
| de1c0adde3 | |||
| 16bbd1b9d0 | |||
| 3220f6d230 | |||
| 9d452ddf11 | |||
| c8fc6faaa3 | |||
| c5b39d6816 | |||
| 057937938d | |||
| 4e920c57ec |
@@ -26,7 +26,7 @@ jobs:
|
|||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7.0.0
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
cache: true
|
cache: true
|
||||||
- name: install chromium (linux)
|
- name: install chromium (linux)
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
uses: browser-actions/setup-chrome@v1
|
uses: browser-actions/setup-chrome@v2.1.2
|
||||||
id: chrome
|
id: chrome
|
||||||
- name: gofmt
|
- name: gofmt
|
||||||
run: |
|
run: |
|
||||||
@@ -67,13 +67,13 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7.0.0
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: true
|
cache: true
|
||||||
- uses: golangci/golangci-lint-action@v8
|
- uses: golangci/golangci-lint-action@v9.2.1
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
govulncheck:
|
govulncheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7.0.0
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
tidy:
|
tidy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7.0.0
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
webview:
|
webview:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7.0.0
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -133,7 +133,7 @@ jobs:
|
|||||||
windows-gui:
|
windows-gui:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7.0.0
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6.0.2
|
- uses: actions/checkout@v7.0.0
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
# Sitemap lastmod comes from the latest content commit.
|
# Sitemap lastmod comes from the latest content commit.
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Checkout tago
|
- name: Checkout tago
|
||||||
uses: actions/checkout@v6.0.2
|
uses: actions/checkout@v7.0.0
|
||||||
with:
|
with:
|
||||||
repository: tamnd/tago
|
repository: tamnd/tago
|
||||||
path: .tago-src
|
path: .tago-src
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
group: cloudflare-pages-kage
|
group: cloudflare-pages-kage
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6.0.2
|
- uses: actions/checkout@v7.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
sparse-checkout: scripts/
|
sparse-checkout: scripts/
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
if: github.ref_type != 'tag'
|
if: github.ref_type != 'tag'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
cache: true
|
cache: true
|
||||||
- uses: goreleaser/goreleaser-action@v6
|
- uses: goreleaser/goreleaser-action@v7.2.2
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: "~> v2"
|
version: "~> v2"
|
||||||
@@ -49,8 +49,10 @@ jobs:
|
|||||||
contents: write # create the GitHub release
|
contents: write # create the GitHub release
|
||||||
packages: write # push the image to ghcr.io
|
packages: write # push the image to ghcr.io
|
||||||
id-token: write # keyless cosign signing
|
id-token: write # keyless cosign signing
|
||||||
|
env:
|
||||||
|
LINUX_REPO_DISPATCH_TOKEN: ${{ secrets.LINUX_REPO_DISPATCH_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v7.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
@@ -60,19 +62,25 @@ jobs:
|
|||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
# Build and ship the linux/arm64 image from the amd64 runner.
|
# Build and ship the linux/arm64 image from the amd64 runner.
|
||||||
- uses: docker/setup-qemu-action@v3
|
- uses: docker/setup-qemu-action@v4.1.0
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v4.1.0
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# Tools GoReleaser shells out to for signing and SBOMs.
|
# Tools GoReleaser shells out to for signing and SBOMs.
|
||||||
- uses: sigstore/cosign-installer@v3
|
# Pin cosign to the 2.x line. cosign 3.x makes the new bundle format the
|
||||||
- uses: anchore/sbom-action/download-syft@v0
|
# default, which ignores the --output-signature/--output-certificate flags
|
||||||
|
# the signs block uses and aborts. Pinning keeps the .sig/.pem outputs and
|
||||||
|
# stops the release tool from floating to a breaking latest.
|
||||||
|
- uses: sigstore/cosign-installer@v4.1.2
|
||||||
|
with:
|
||||||
|
cosign-release: "v2.6.3"
|
||||||
|
- uses: anchore/sbom-action/download-syft@v0.24.0
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@v6
|
- uses: goreleaser/goreleaser-action@v7.2.2
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: "~> v2"
|
version: "~> v2"
|
||||||
@@ -85,3 +93,20 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||||
SCOOP_BUCKET_GITHUB_TOKEN: ${{ secrets.SCOOP_BUCKET_GITHUB_TOKEN }}
|
SCOOP_BUCKET_GITHUB_TOKEN: ${{ secrets.SCOOP_BUCKET_GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Rebuild the Linux apt/dnf repository with the packages just released.
|
||||||
|
# Skipped when the dispatch token is unset, so the release never depends
|
||||||
|
# on it being configured.
|
||||||
|
- name: Refresh the Linux package repository
|
||||||
|
if: env.LINUX_REPO_DISPATCH_TOKEN != ''
|
||||||
|
run: |
|
||||||
|
code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: Bearer $LINUX_REPO_DISPATCH_TOKEN" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
https://api.github.com/repos/tamnd/linux-repo/dispatches \
|
||||||
|
-d '{"event_type":"package-released"}' || true)
|
||||||
|
if [ "$code" = "204" ]; then
|
||||||
|
echo "Linux repo refresh triggered"
|
||||||
|
else
|
||||||
|
echo "Linux repo refresh not accepted (HTTP $code); skipping"
|
||||||
|
fi
|
||||||
|
|||||||
+5
-4
@@ -151,11 +151,11 @@ homebrew_casks:
|
|||||||
repository:
|
repository:
|
||||||
owner: tamnd
|
owner: tamnd
|
||||||
name: homebrew-tap
|
name: homebrew-tap
|
||||||
token: '{{ envOrDefault "HOMEBREW_TAP_GITHUB_TOKEN" "" }}'
|
token: '{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}'
|
||||||
directory: Casks
|
directory: Casks
|
||||||
homepage: https://github.com/tamnd/kage
|
homepage: https://github.com/tamnd/kage
|
||||||
description: Clone any website for offline viewing, with the JavaScript stripped out
|
description: Clone any website for offline viewing, with the JavaScript stripped out
|
||||||
skip_upload: '{{ if envOrDefault "HOMEBREW_TAP_GITHUB_TOKEN" "" }}false{{ else }}true{{ end }}'
|
skip_upload: '{{ if index .Env "HOMEBREW_TAP_GITHUB_TOKEN" }}false{{ else }}true{{ end }}'
|
||||||
commit_author:
|
commit_author:
|
||||||
name: Duc-Tam Nguyen
|
name: Duc-Tam Nguyen
|
||||||
email: tamnd87@gmail.com
|
email: tamnd87@gmail.com
|
||||||
@@ -168,11 +168,12 @@ scoops:
|
|||||||
repository:
|
repository:
|
||||||
owner: tamnd
|
owner: tamnd
|
||||||
name: scoop-bucket
|
name: scoop-bucket
|
||||||
token: '{{ envOrDefault "SCOOP_BUCKET_GITHUB_TOKEN" "" }}'
|
token: '{{ .Env.SCOOP_BUCKET_GITHUB_TOKEN }}'
|
||||||
|
directory: bucket
|
||||||
homepage: https://github.com/tamnd/kage
|
homepage: https://github.com/tamnd/kage
|
||||||
description: Clone any website for offline viewing, with the JavaScript stripped out
|
description: Clone any website for offline viewing, with the JavaScript stripped out
|
||||||
license: MIT
|
license: MIT
|
||||||
skip_upload: '{{ if envOrDefault "SCOOP_BUCKET_GITHUB_TOKEN" "" }}false{{ else }}true{{ end }}'
|
skip_upload: '{{ if index .Env "SCOOP_BUCKET_GITHUB_TOKEN" }}false{{ else }}true{{ end }}'
|
||||||
commit_author:
|
commit_author:
|
||||||
name: Duc-Tam Nguyen
|
name: Duc-Tam Nguyen
|
||||||
email: tamnd87@gmail.com
|
email: tamnd87@gmail.com
|
||||||
|
|||||||
@@ -6,6 +6,19 @@ All notable changes to kage are recorded here. The format follows
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.3.6] - 2026-06-19
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `kage clone --mobile` now produces a correct layout when the source page uses table-based navigation with image maps, as paulgraham.com does.
|
||||||
|
Three rendering problems appeared in Kiwix iOS after the 0.3.5 release: a tall blank box at the top of every page, content clipped on the right edge, and a narrow spacer column occupying screen space.
|
||||||
|
The blank box came from hiding only the `<img usemap>` element while leaving its `<td>` container in place; the cell collapsed to empty but kept its full height.
|
||||||
|
`td:has(>img[usemap])` now hides the entire nav column rather than just the image inside it.
|
||||||
|
The right-side clip came from `width="435"` set directly as an HTML attribute on the inner content table; the earlier `max-width` CSS rule does not override HTML attributes.
|
||||||
|
A new `[width]{width:auto!important;max-width:100%!important}` rule cancels every fixed HTML width attribute on any element — tables, cells, and images alike — so the content column stretches to fill the phone screen.
|
||||||
|
The spacer column (a 26 px `<td>` holding a 1×1 transparent GIF) kept its allocated width for the same reason; `td:has(>img[src*="trans_1x1"]:only-child)` hides it alongside the nav column.
|
||||||
|
Two additional rules round out the fix: `*{box-sizing:border-box}` prevents padding from pushing content past the viewport edge, and `img{max-width:100%;height:auto}` keeps any inline photos within their column.
|
||||||
|
|
||||||
## [0.3.5] - 2026-06-19
|
## [0.3.5] - 2026-06-19
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -24,7 +24,27 @@ Full docs and guides live at **[kage.tamnd.com](https://kage.tamnd.com)**.
|
|||||||
go install github.com/tamnd/kage/cmd/kage@latest
|
go install github.com/tamnd/kage/cmd/kage@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Prefer a prebuilt binary? Grab an archive, a `.deb`/`.rpm`/`.apk`, or a checksum from [releases](https://github.com/tamnd/kage/releases). Or skip installing Chrome yourself and use the container image, which bundles Chromium:
|
Prefer a prebuilt binary? Grab an archive, a `.deb`/`.rpm`/`.apk`, or a checksum from [releases](https://github.com/tamnd/kage/releases). Or let a package manager handle it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Homebrew (macOS)
|
||||||
|
brew install tamnd/tap/kage
|
||||||
|
|
||||||
|
# Scoop (Windows)
|
||||||
|
scoop bucket add tamnd https://github.com/tamnd/scoop-bucket
|
||||||
|
scoop install kage
|
||||||
|
|
||||||
|
# apt (Debian, Ubuntu)
|
||||||
|
curl -fsSL https://tamnd.github.io/linux-repo/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/tamnd.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/tamnd.gpg] https://tamnd.github.io/linux-repo/apt stable main" | sudo tee /etc/apt/sources.list.d/tamnd.list
|
||||||
|
sudo apt update && sudo apt install kage
|
||||||
|
|
||||||
|
# dnf (Fedora, RHEL)
|
||||||
|
sudo dnf config-manager --add-repo https://tamnd.github.io/linux-repo/dnf/tamnd.repo
|
||||||
|
sudo dnf install kage
|
||||||
|
```
|
||||||
|
|
||||||
|
Or skip installing Chrome yourself and use the container image, which bundles Chromium:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm -v "$PWD/out:/out" ghcr.io/tamnd/kage clone paulgraham.com
|
docker run --rm -v "$PWD/out:/out" ghcr.io/tamnd/kage clone paulgraham.com
|
||||||
|
|||||||
+31
-31
@@ -16,37 +16,37 @@ import (
|
|||||||
|
|
||||||
// cloneFlags holds the parsed flag values for one invocation.
|
// cloneFlags holds the parsed flag values for one invocation.
|
||||||
type cloneFlags struct {
|
type cloneFlags struct {
|
||||||
out string
|
out string
|
||||||
reserved string
|
reserved string
|
||||||
workers int
|
workers int
|
||||||
assetWorkers int
|
assetWorkers int
|
||||||
browserPages int
|
browserPages int
|
||||||
maxPages int
|
maxPages int
|
||||||
maxDepth int
|
maxDepth int
|
||||||
traversal string
|
traversal string
|
||||||
maxAssetMB int64
|
maxAssetMB int64
|
||||||
keepMedia bool
|
keepMedia bool
|
||||||
skipExt []string
|
skipExt []string
|
||||||
allAssetHosts bool
|
allAssetHosts bool
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
settle time.Duration
|
settle time.Duration
|
||||||
renderTO time.Duration
|
renderTO time.Duration
|
||||||
scroll bool
|
scroll bool
|
||||||
userAgent string
|
userAgent string
|
||||||
subdomains bool
|
subdomains bool
|
||||||
scopePrefix string
|
scopePrefix string
|
||||||
exclude []string
|
exclude []string
|
||||||
noRobots bool
|
noRobots bool
|
||||||
noSitemap bool
|
noSitemap bool
|
||||||
headful bool
|
headful bool
|
||||||
keepNoscript bool
|
keepNoscript bool
|
||||||
mobileReadable bool
|
mobileReadable bool
|
||||||
chromeBin string
|
chromeBin string
|
||||||
controlURL string
|
controlURL string
|
||||||
noResume bool
|
noResume bool
|
||||||
refresh bool
|
refresh bool
|
||||||
force bool
|
force bool
|
||||||
quiet bool
|
quiet bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCloneCmd() *cobra.Command {
|
func newCloneCmd() *cobra.Command {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Installation"
|
title: "Installation"
|
||||||
description: "Install kage from Go, Homebrew, a release archive, a Linux package, or the container image, and point it at a browser."
|
description: "Install kage from Go, Homebrew, Scoop, a release archive, a Linux package, or the container image, and point it at a browser."
|
||||||
weight: 20
|
weight: 20
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -12,26 +12,54 @@ kage is a single binary. Pick whichever channel suits you.
|
|||||||
go install github.com/tamnd/kage/cmd/kage@latest
|
go install github.com/tamnd/kage/cmd/kage@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Homebrew
|
## Homebrew (macOS)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install tamnd/tap/kage
|
brew install tamnd/tap/kage
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The cask installs the prebuilt macOS binary. On Linux, use the packages below or
|
||||||
|
`go install`.
|
||||||
|
|
||||||
|
## Scoop (Windows)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scoop bucket add tamnd https://github.com/tamnd/scoop-bucket
|
||||||
|
scoop install kage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linux (apt and dnf)
|
||||||
|
|
||||||
|
A signed apt and dnf repository tracks every release, so `apt upgrade` and
|
||||||
|
`dnf upgrade` keep kage current.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian, Ubuntu
|
||||||
|
curl -fsSL https://tamnd.github.io/linux-repo/gpg.key \
|
||||||
|
| sudo gpg --dearmor -o /usr/share/keyrings/tamnd.gpg
|
||||||
|
echo "deb [signed-by=/usr/share/keyrings/tamnd.gpg] https://tamnd.github.io/linux-repo/apt stable main" \
|
||||||
|
| sudo tee /etc/apt/sources.list.d/tamnd.list
|
||||||
|
sudo apt update && sudo apt install kage
|
||||||
|
|
||||||
|
# Fedora, RHEL
|
||||||
|
sudo dnf config-manager --add-repo https://tamnd.github.io/linux-repo/dnf/tamnd.repo
|
||||||
|
sudo dnf install kage
|
||||||
|
```
|
||||||
|
|
||||||
## Release archives and Linux packages
|
## Release archives and Linux packages
|
||||||
|
|
||||||
Every [release](https://github.com/tamnd/kage/releases) attaches `tar.gz`
|
Every [release](https://github.com/tamnd/kage/releases) attaches `tar.gz`
|
||||||
archives (and a `.zip` for Windows) for Linux, macOS, Windows, and FreeBSD, plus
|
archives (and a `.zip` for Windows) for Linux, macOS, Windows, and FreeBSD, plus
|
||||||
`.deb`, `.rpm`, and `.apk` packages and a `checksums.txt` with a cosign
|
`.deb`, `.rpm`, and `.apk` packages and a `checksums.txt` with a cosign
|
||||||
signature. Download the one for your platform, extract `kage`, and put it on your
|
signature. Download the one for your platform, extract `kage`, and put it on your
|
||||||
`PATH`.
|
`PATH`. To install a package directly without the repo above:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Debian/Ubuntu
|
# Debian/Ubuntu
|
||||||
sudo dpkg -i kage_*_linux_amd64.deb
|
sudo dpkg -i kage_*_amd64.deb
|
||||||
|
|
||||||
# Fedora/RHEL
|
# Fedora/RHEL
|
||||||
sudo rpm -i kage_*_linux_amd64.rpm
|
sudo rpm -i kage-*.x86_64.rpm
|
||||||
```
|
```
|
||||||
|
|
||||||
## Container
|
## Container
|
||||||
|
|||||||
+29
-13
@@ -303,21 +303,37 @@ func findElement(n *html.Node, a atom.Atom) *html.Node {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mobileCSS is injected when MobileReadable is set. It targets legacy "font
|
// mobileCSS is injected when MobileReadable is set. It rewrites font-era
|
||||||
// era" HTML that renders as microscopic text on mobile:
|
// HTML for comfortable reading on a phone. Key rules:
|
||||||
// - :root font-size 18 px — baseline all em/rem sizes upward
|
//
|
||||||
// - body — centre, cap width, add padding, loosen line height
|
// - box-sizing:border-box — makes padding predictable in a layout built with
|
||||||
// - font element — override the in-HTML size/face attributes that sites like
|
// HTML width attributes, so our padding doesn't cause overflow.
|
||||||
// paulgraham.com embed directly in the markup (e.g. <font size="2">)
|
// - body — no fixed max-width here; let the table rules handle width instead.
|
||||||
// - table/td — prevent overflow; add minimal cell breathing room
|
// overflow-x:hidden catches any stray overflow without a scrollbar.
|
||||||
// - img[usemap], map — image-map navigation is useless offline (the image
|
// - font element — overrides in-HTML size/face attributes (e.g. <font size="2">).
|
||||||
// itself usually 404s from an external CDN); hide both the image and the map
|
// - [width],[height] — cancels all HTML attribute widths/heights on every
|
||||||
const mobileCSS = `body{max-width:720px;margin:0 auto;padding:.75em 1em;line-height:1.7;font-family:Georgia,"Times New Roman",serif}` +
|
// element (tables, tds, imgs, etc.) so fixed-pixel columns become fluid.
|
||||||
|
// - table — fluid, auto layout, no horizontal scroll.
|
||||||
|
// - td — auto width so a three-column table (nav | spacer | content) collapses
|
||||||
|
// to one usable column once the nav td is hidden.
|
||||||
|
// - img — responsive: never wider than its container.
|
||||||
|
// - td:has(>img[usemap]) — hides the entire nav column td, not just the image
|
||||||
|
// inside it; hiding only the img left a tall empty white box.
|
||||||
|
// - td:has(>img[src*="trans_1x1"]) — hides the 26 px spacer column td (a 1×1
|
||||||
|
// transparent GIF whose only job was spacing in the original table layout).
|
||||||
|
const mobileCSS = `*{box-sizing:border-box}` +
|
||||||
`:root{font-size:18px}` +
|
`:root{font-size:18px}` +
|
||||||
|
`body{margin:0;padding:.75em 1em;line-height:1.7;font-family:Georgia,"Times New Roman",serif;overflow-x:hidden}` +
|
||||||
`font{font-size:1rem!important;font-family:inherit!important;color:inherit!important}` +
|
`font{font-size:1rem!important;font-family:inherit!important;color:inherit!important}` +
|
||||||
`table{max-width:100%!important;word-break:break-word}` +
|
`[width]{width:auto!important;max-width:100%!important}` +
|
||||||
`td,th{padding:.25em!important}` +
|
`[height]{height:auto!important}` +
|
||||||
`img[usemap],map{display:none!important}`
|
`table{width:100%!important;max-width:100%!important;table-layout:auto!important;border-collapse:collapse!important;word-break:break-word}` +
|
||||||
|
`td,th{width:auto!important;max-width:100%!important;padding:.35em .5em!important;vertical-align:top!important;overflow-wrap:break-word}` +
|
||||||
|
`img{max-width:100%!important;height:auto!important}` +
|
||||||
|
`img[usemap],map{display:none!important}` +
|
||||||
|
`td:has(>img[usemap]),td:has(>map){display:none!important}` +
|
||||||
|
`img[src*="trans_1x1"],img[src*="spacer"],img[height="1"],img[width="1"]{display:none!important}` +
|
||||||
|
`td:has(>img[src*="trans_1x1"]:only-child),td:has(>img[height="1"]:only-child){display:none!important}`
|
||||||
|
|
||||||
// ensureViewport inserts <meta name="viewport" content="width=device-width,
|
// ensureViewport inserts <meta name="viewport" content="width=device-width,
|
||||||
// initial-scale=1"> at the top of <head> when the document does not already
|
// initial-scale=1"> at the top of <head> when the document does not already
|
||||||
|
|||||||
@@ -218,6 +218,40 @@ func TestMobileReadableInjectsViewportAndCSS(t *testing.T) {
|
|||||||
if !strings.Contains(s, "font{font-size:1rem") {
|
if !strings.Contains(s, "font{font-size:1rem") {
|
||||||
t.Error("font override not in mobile CSS")
|
t.Error("font override not in mobile CSS")
|
||||||
}
|
}
|
||||||
|
// Fluid table rules must be present.
|
||||||
|
if !strings.Contains(s, "table{width:100%") {
|
||||||
|
t.Error("fluid table rule missing")
|
||||||
|
}
|
||||||
|
// [width] override must be present so fixed HTML width attributes are cancelled.
|
||||||
|
if !strings.Contains(s, "[width]{width:auto") {
|
||||||
|
t.Error("[width] override missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMobileReadableHidesNavColumnTd(t *testing.T) {
|
||||||
|
// paulgraham.com wraps its image-map nav in a <td>. Hiding only the img
|
||||||
|
// leaves a tall empty box; the CSS must also target the containing td.
|
||||||
|
in := `<html><head><title>T</title></head><body>` +
|
||||||
|
`<table><tr>` +
|
||||||
|
`<td><map name="nav"><area shape="rect" coords="0,0,67,21" href="index.html"></map>` +
|
||||||
|
`<img src="nav.gif" width="69" height="357" usemap="#nav"/></td>` +
|
||||||
|
`<td><img src="https://cdn.example.com/trans_1x1.gif" height="1" width="26"/></td>` +
|
||||||
|
`<td><p>Essay text.</p></td>` +
|
||||||
|
`</tr></table>` +
|
||||||
|
`</body></html>`
|
||||||
|
out, _, err := Strip([]byte(in), Options{MobileReadable: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := string(out)
|
||||||
|
// td:has(>img[usemap]) rule must be present so the whole nav column is hidden.
|
||||||
|
if !strings.Contains(s, "td:has(>img[usemap])") {
|
||||||
|
t.Error("td:has(>img[usemap]) rule missing from mobile CSS")
|
||||||
|
}
|
||||||
|
// td:has(>img[src*="trans_1x1"]) rule must be present for spacer column.
|
||||||
|
if !strings.Contains(s, `td:has(>img[src*="trans_1x1"]`) {
|
||||||
|
t.Error(`td:has(>img[src*="trans_1x1"]) spacer-column rule missing from mobile CSS`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMobileReadableSkipsExistingViewport(t *testing.T) {
|
func TestMobileReadableSkipsExistingViewport(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user