Skip to content

Commit f54ed24

Browse files
authored
Merge pull request #144 from KubaO/staging
Improvements to the build process.
2 parents 68c04be + fbdb715 commit f54ed24

13 files changed

Lines changed: 943 additions & 232 deletions

File tree

.github/workflows/checks.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
uses: actions/setup-python@v5
5151
with:
5252
python-version: '3.14'
53+
cache: 'pip'
5354
- name: Install Python deps
5455
run: pip install -r requirements.txt
5556
- name: Check offline links (check_links.py)

.github/workflows/jekyll-gh-pages.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ jobs:
8686
uses: actions/setup-python@v5
8787
with:
8888
python-version: '3.14'
89+
cache: 'pip'
8990
- name: Install Python deps
9091
run: pip install -r requirements.txt
9192
- name: Check offline links (check_links.py)

docs/Features/Fusion.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ If one or more controls are not registered for the current architecture then twi
6666

6767
When this occurs, you will see a note in the DEBUG CONSOLE:
6868

69-
<img width="412" height="73" alt="tbFusionDebugConsole" src="Images/569099635-bc9553a6-fcce-487d-a478-dbee557f33b1.png" />
69+
![tbFusionDebugConsole](Images/569099635-bc9553a6-fcce-487d-a478-dbee557f33b1.png){:width="412" height="73"}
7070

7171
This additional EXE acts as the out-of-process container for those controls and is managed automatically by the twinBASIC IDE
7272

@@ -76,7 +76,7 @@ A project-level setting allows you to control where the Fusion host EXE is gener
7676

7777
- **ActiveX Fusion Host EXE Output Path**
7878

79-
<img width="800" height="400" alt="tbFusionProjectSettings" src="Images/569150839-9ffc87ac-250d-40a4-bb47-669b607ad76f.png" />
79+
![tbFusionProjectSettings](Images/569150839-9ffc87ac-250d-40a4-bb47-669b607ad76f.png){:width="800" height="400"}
8080

8181
If left blank (default), the standard build path set in the project settings is used. Unless overriden, the standard build path is:
8282
${SourcePath}\Build${ProjectName}_${Architecture}.${FileExtension}
@@ -91,7 +91,7 @@ This allows Fusion host EXEs to be clearly distinguished from normal build outpu
9191

9292
Each COM reference (type library) exposes Fusion-specific options.
9393

94-
<img width="737" height="323" alt="tbFusionPerLibraryOptions" src="Images/569100769-f1f2790a-0094-4843-809f-a8a9e928fd41.png" />
94+
![tbFusionPerLibraryOptions](Images/569100769-f1f2790a-0094-4843-809f-a8a9e928fd41.png){:width="737" height="323"}
9595

9696
### ActiveX Fusion Mode
9797

docs/Reference/Attributes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ Calculate implicit enum values as a flag set (powers of 2).
369369
> [!NOTE]
370370
> To prevent confusion, once an explicit value is used, all remaining values after it must also be explicit)
371371
372-
![image](Images/flags attribute.png)
372+
![image](Images/flags-attribute.png)
373373

374374
## FloatingPointErrorChecks (optional Bool)
375375
{: #floatingpointerrorchecks }

docs/_includes/book-chapter-body.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,19 @@
8888
{%- endif -%}
8989
{%- endunless -%}
9090

91+
{%- comment -%}
92+
Strip the `src="<baseurl>/` prefix that `relative_url` injects when
93+
`jekyll build --baseurl /<repo>` is passed (the CI deploy path uses
94+
this for Pages project sites without a custom domain). With empty
95+
baseurl the prefix collapses to `src="/`, matching the historical
96+
leading-slash strip exactly. Once stripped, image paths inside
97+
book.html are root-of-_site/-relative, which is what both pdfify's
98+
source lookup and pagedjs's render-time fetch expect.
99+
{%- endcomment -%}
100+
{%- assign src_baseurl_strip = 'src="' | append: site.baseurl | append: '/' -%}
101+
91102
{%- assign body = body
92-
| replace: 'src="/', 'src="'
103+
| replace: src_baseurl_strip, 'src="'
93104
| replace: p1_search, p1_replace
94105
| replace: p2_search, p2_replace
95106
| replace: p3i12_search, p3i12_replace

docs/_plugins/book-href-rewrite.rb

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,28 @@ def self.resolve_href(href, parent_url)
189189
nil
190190
end
191191

192+
# Normalise `site.config["baseurl"]` to either "" or "/segment..."
193+
# (no trailing slash) -- the exact prefix `relative_url` actually
194+
# injects into rendered HTML. Mirrors `Offlinify.normalize_baseurl`;
195+
# duplicated rather than cross-required to keep plugins independent.
196+
def self.normalize_baseurl(raw_baseurl)
197+
baseurl = (raw_baseurl || "").to_s.sub(%r{/+\z}, "")
198+
baseurl = "/#{baseurl}" if !baseurl.empty? && !baseurl.start_with?("/")
199+
baseurl
200+
end
201+
202+
# Strip the baseurl prefix from a root-absolute path so the result
203+
# matches the keys in `url_to_anchor` (which are built from
204+
# `page.url` -- baseurl-less). Two forms are handled: the exact
205+
# baseurl alone (`/twinBASIC-docs` -> `/`), and a normal subpath
206+
# (`/twinBASIC-docs/foo` -> `/foo`). Anything else passes through.
207+
def self.strip_baseurl(path, baseurl)
208+
return path if baseurl.empty?
209+
return "/" if path == baseurl
210+
return path[baseurl.length..] if path.start_with?(baseurl + "/")
211+
path
212+
end
213+
192214
# Rewrite every `href="..."` in the article body. External and
193215
# already-in-book anchor hrefs (`http`, `mailto:`, `#...`) pass
194216
# through unchanged; the `#...` form has already been chapter-anchor
@@ -202,7 +224,12 @@ def self.resolve_href(href, parent_url)
202224
# only the map-lookup step was selective). Keeps build output
203225
# byte-comparable and makes broken out-of-book links easier to grep
204226
# for during verification.
205-
def self.rewrite_body(body, parent_url, url_to_anchor)
227+
#
228+
# `baseurl` is the normalised `site.config["baseurl"]`; when CI runs
229+
# `jekyll build --baseurl /<repo>` the `relative_url`-emitted hrefs
230+
# carry that prefix and must be stripped before the lookup, since
231+
# `url_to_anchor` keys come from `page.url` (baseurl-less).
232+
def self.rewrite_body(body, parent_url, url_to_anchor, baseurl)
206233
body.gsub(/href="([^"]*)"/) do |whole_match|
207234
href = Regexp.last_match(1)
208235
next whole_match if EXTERNAL_PREFIXES.any? { |pfx| href.start_with?(pfx) }
@@ -211,11 +238,18 @@ def self.rewrite_body(body, parent_url, url_to_anchor)
211238
next whole_match unless abs && abs.start_with?("/")
212239

213240
path_part, frag_part = abs.split("#", 2)
214-
target = url_to_anchor[path_part]
241+
lookup_path = strip_baseurl(path_part, baseurl)
242+
target = url_to_anchor[lookup_path]
215243
if target
216244
frag_part ? %(href="##{target}-#{frag_part}") : %(href="##{target}")
217245
else
218-
%(href="#{abs}")
246+
# Out-of-book target: emit the baseurl-stripped form so the
247+
# URL the PDF reader displays is stable across local builds
248+
# and the `--baseurl /<repo>` CI deploy path. Dead in the PDF
249+
# either way, but the canonical (baseurl-less) form is what
250+
# matches the live site URL when read offline.
251+
miss_path = frag_part ? "#{lookup_path}##{frag_part}" : lookup_path
252+
%(href="#{miss_path}")
219253
end
220254
end
221255
end
@@ -227,6 +261,7 @@ def self.process(page)
227261
parent_map = build_anchor_to_parent(site)
228262
return if parent_map.empty?
229263
landing_anchors = build_landing_anchors(site)
264+
baseurl = normalize_baseurl(site.config["baseurl"])
230265

231266
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
232267

@@ -248,7 +283,7 @@ def self.process(page)
248283

249284
parent_url = parent_map[anchor_id]
250285
if parent_url
251-
new_body = rewrite_body(body, parent_url, url_to_anchor)
286+
new_body = rewrite_body(body, parent_url, url_to_anchor, baseurl)
252287
rewritten += 1 if new_body != body
253288
body = new_body
254289
end

docs/_plugins/jekyll-relative-links-patch.rb

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# frozen_string_literal: true
22

33
# Patch for jekyll-relative-links (>=0.7.0): replace the O(N) linear
4-
# scan in `url_for_path` with an O(1) hash lookup.
4+
# scan in `url_for_path` with an O(1) hash lookup, and extend lookup
5+
# to consult `permalink:` frontmatter and `redirect_from:` aliases
6+
# when a file-path match misses.
57
#
6-
# === The bug ===
8+
# === The perf bug ===
79
#
810
# `JekyllRelativeLinks::Generator#url_for_path` is invoked once for
911
# every markdown link match (both inline `[X](Y)` and reference-style
@@ -29,34 +31,67 @@
2931
# bulk of GENERATE on a build that otherwise takes ~600ms in that
3032
# phase.
3133
#
32-
# === The fix ===
34+
# The perf fix builds a hash from `relative_path` (leading slash
35+
# stripped, matching the unpatched comparison) to the target object
36+
# once, and looks up by key thereafter. O(M*N) -> O(M+N). First-wins
37+
# semantics (`unless h.key?(key)`) match the unpatched `.find`.
3338
#
34-
# Build a hash from `relative_path` (with the leading slash stripped,
35-
# to match the unpatched comparison) to the target object once, and
36-
# look up by key thereafter. Hash construction is O(N) once; each
37-
# subsequent lookup is O(1). Total cost drops from O(M*N) to O(M+N),
38-
# and the GENERATE phase shrinks accordingly.
39+
# === The semantic gap ===
3940
#
40-
# The hash is built with first-wins semantics (`unless h.key?(key)`)
41-
# to match the unpatched `.find`, which returns the first matching
42-
# target. In practice `relative_path` is unique across pages, static
43-
# files, and docs, so this only matters as defence against an
44-
# unexpected duplicate -- but matching the upstream behaviour exactly
45-
# keeps the patch a safe drop-in.
41+
# Upstream only matches the link path against `relative_path` (the
42+
# file's on-disk path). Pages that use `permalink:` frontmatter to
43+
# rename their URL slug are invisible to the gem -- e.g. source
44+
# `[twinBASIC Videos](Videos/tB)` targets `docs/Videos/twinBASIC.md`
45+
# (`permalink: /Videos/tB`), but the gem looks for `Videos/tB.md`,
46+
# doesn't find one, and leaves the link unrewritten. The rendered
47+
# HTML keeps the relative path, which works online only by accident
48+
# of relative-path math, and falls back further on `redirect_from:`
49+
# stubs as an undocumented safety net. In the PDF book (where chapter
50+
# bodies get concatenated under `/book.html`) the same relative path
51+
# can no longer reach the target page, and the rewriter that turns
52+
# in-book hrefs into chapter anchors can't match the unresolved form
53+
# either -- so cross-references break.
54+
#
55+
# The fix adds two fallback hashes after the file-path table:
56+
#
57+
# potential_targets_by_url keys: leading-slash-stripped
58+
# `page.url`. Both with- and
59+
# without-trailing-slash forms
60+
# are indexed for folder-style
61+
# index pages whose permalinks
62+
# end in `/`, so
63+
# `[X](Tutorials/CEF)` and
64+
# `[X](Tutorials/CEF/)` both
65+
# resolve.
66+
#
67+
# potential_targets_by_redirect_from keys: leading-slash-stripped,
68+
# trailing-slash-trimmed
69+
# `redirect_from` aliases.
70+
# Returns the target page
71+
# whose canonical permalink is
72+
# `page.url`, so url_for_path
73+
# emits the canonical form
74+
# rather than relying on the
75+
# redirect stub at runtime.
76+
#
77+
# `url_for_path` chains all three: file-path first (upstream behaviour
78+
# -- author-intended file references always win), then permalink, then
79+
# redirect_from. First hit wins. Misses still return nil and the gem
80+
# leaves the link unrewritten, matching upstream's fail-open contract.
4681
#
4782
# === Compatibility ===
4883
#
4984
# Targets the upstream gem version pinned by Gemfile.lock (0.7.0). The
50-
# patch overrides only `url_for_path` and adds one new memoiser
51-
# (`potential_targets_by_path`); every other method is untouched. The
52-
# `unless method_defined?` guard makes the patch idempotent against
53-
# accidental double-load.
85+
# patch overrides only `url_for_path` and adds three new memoisers
86+
# (`potential_targets_by_path`, `..._by_url`, `..._by_redirect_from`);
87+
# every other method is untouched. The `unless method_defined?` guard
88+
# makes the patch idempotent against accidental double-load.
5489
#
5590
# If a future release rewrites `url_for_path`, re-verify that the
5691
# replacement still resolves a path to a target by scanning
57-
# `potential_targets` (or an equivalent) and that swapping in a hash
58-
# lookup remains a faithful drop-in. If the upstream project takes a
59-
# PR for this, delete this file.
92+
# `potential_targets` (or an equivalent) and that swapping in the
93+
# three-tier hash lookup remains a faithful extension. If the upstream
94+
# project takes a PR for this, delete this file.
6095

6196
require "jekyll-relative-links"
6297

@@ -70,9 +105,66 @@ def potential_targets_by_path
70105
end
71106
end
72107

108+
# Pages indexed by their rendered URL (permalink), leading slash
109+
# stripped to match the form `path_from_root` produces. Folder-
110+
# style permalinks (URL ending in `/`) are also indexed under
111+
# their trimmed form so source markdown can drop the trailing
112+
# slash. Restricted to pages and writable docs -- static files
113+
# have a `url` but it's just the file path, which the by_path
114+
# table already covers.
115+
#
116+
# `JekyllRedirectFrom::RedirectPage` instances are excluded:
117+
# the jekyll-redirect-from plugin synthesizes a stub page for
118+
# every `redirect_from` alias, each with `url` equal to the
119+
# alias itself. Indexing those would route source links through
120+
# the redirect stub (a one-hop intermediate that only works in
121+
# a browser) instead of resolving straight to the canonical
122+
# target. The `by_redirect_from` table below indexes the same
123+
# aliases but points at the canonical page, which is what we
124+
# want.
125+
def potential_targets_by_url
126+
@potential_targets_by_url ||= begin
127+
is_redirect_stub = defined?(JekyllRedirectFrom::RedirectPage) \
128+
? ->(p) { p.is_a?(JekyllRedirectFrom::RedirectPage) } \
129+
: ->(_p) { false }
130+
(site.pages + site.docs_to_write).each_with_object({}) do |p, h|
131+
next if is_redirect_stub.call(p)
132+
url = p.url.to_s
133+
next if url.empty? || url == "/"
134+
key = url.sub(%r!\A/!, "")
135+
h[key] = p unless h.key?(key)
136+
if key.end_with?("/")
137+
alt = key.chomp("/")
138+
h[alt] = p unless h.key?(alt)
139+
end
140+
end
141+
end
142+
end
143+
144+
# Pages indexed by their `redirect_from` aliases (set by the
145+
# jekyll-redirect-from plugin). Each alias is normalised to the
146+
# leading-slash-stripped, trailing-slash-trimmed form so source
147+
# markdown using a historical URL (e.g. a moved page's old slug)
148+
# resolves to the page's current canonical URL.
149+
def potential_targets_by_redirect_from
150+
@potential_targets_by_redirect_from ||= begin
151+
(site.pages + site.docs_to_write).each_with_object({}) do |p, h|
152+
Array(p.data["redirect_from"]).each do |alias_url|
153+
alias_str = alias_url.to_s
154+
next if alias_str.empty?
155+
key = alias_str.sub(%r!\A/!, "").chomp("/")
156+
next if key.empty?
157+
h[key] = p unless h.key?(key)
158+
end
159+
end
160+
end
161+
end
162+
73163
def url_for_path(path)
74164
path = CGI.unescape(path)
75-
target = potential_targets_by_path[path]
165+
target = potential_targets_by_path[path] ||
166+
potential_targets_by_url[path.chomp("/")] ||
167+
potential_targets_by_redirect_from[path.chomp("/")]
76168
relative_url(target.url) if target&.url
77169
end
78170
end

0 commit comments

Comments
 (0)