Skip to content
Merged
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/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: '3.14'
cache: 'pip'
- name: Install Python deps
run: pip install -r requirements.txt
- name: Check offline links (check_links.py)
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/jekyll-gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: '3.14'
cache: 'pip'
- name: Install Python deps
run: pip install -r requirements.txt
- name: Check offline links (check_links.py)
Expand Down
6 changes: 3 additions & 3 deletions docs/Features/Fusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ If one or more controls are not registered for the current architecture then twi

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

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

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

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

- **ActiveX Fusion Host EXE Output Path**

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

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

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

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

### ActiveX Fusion Mode

Expand Down
2 changes: 1 addition & 1 deletion docs/Reference/Attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ Calculate implicit enum values as a flag set (powers of 2).
> [!NOTE]
> To prevent confusion, once an explicit value is used, all remaining values after it must also be explicit)

![image](Images/flags attribute.png)
![image](Images/flags-attribute.png)

## FloatingPointErrorChecks (optional Bool)
{: #floatingpointerrorchecks }
Expand Down
13 changes: 12 additions & 1 deletion docs/_includes/book-chapter-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,19 @@
{%- endif -%}
{%- endunless -%}

{%- comment -%}
Strip the `src="<baseurl>/` prefix that `relative_url` injects when
`jekyll build --baseurl /<repo>` is passed (the CI deploy path uses
this for Pages project sites without a custom domain). With empty
baseurl the prefix collapses to `src="/`, matching the historical
leading-slash strip exactly. Once stripped, image paths inside
book.html are root-of-_site/-relative, which is what both pdfify's
source lookup and pagedjs's render-time fetch expect.
{%- endcomment -%}
{%- assign src_baseurl_strip = 'src="' | append: site.baseurl | append: '/' -%}

{%- assign body = body
| replace: 'src="/', 'src="'
| replace: src_baseurl_strip, 'src="'
| replace: p1_search, p1_replace
| replace: p2_search, p2_replace
| replace: p3i12_search, p3i12_replace
Expand Down
43 changes: 39 additions & 4 deletions docs/_plugins/book-href-rewrite.rb
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,28 @@ def self.resolve_href(href, parent_url)
nil
end

# Normalise `site.config["baseurl"]` to either "" or "/segment..."
# (no trailing slash) -- the exact prefix `relative_url` actually
# injects into rendered HTML. Mirrors `Offlinify.normalize_baseurl`;
# duplicated rather than cross-required to keep plugins independent.
def self.normalize_baseurl(raw_baseurl)
baseurl = (raw_baseurl || "").to_s.sub(%r{/+\z}, "")
baseurl = "/#{baseurl}" if !baseurl.empty? && !baseurl.start_with?("/")
baseurl
end

# Strip the baseurl prefix from a root-absolute path so the result
# matches the keys in `url_to_anchor` (which are built from
# `page.url` -- baseurl-less). Two forms are handled: the exact
# baseurl alone (`/twinBASIC-docs` -> `/`), and a normal subpath
# (`/twinBASIC-docs/foo` -> `/foo`). Anything else passes through.
def self.strip_baseurl(path, baseurl)
return path if baseurl.empty?
return "/" if path == baseurl
return path[baseurl.length..] if path.start_with?(baseurl + "/")
path
end

# Rewrite every `href="..."` in the article body. External and
# already-in-book anchor hrefs (`http`, `mailto:`, `#...`) pass
# through unchanged; the `#...` form has already been chapter-anchor
Expand All @@ -202,7 +224,12 @@ def self.resolve_href(href, parent_url)
# only the map-lookup step was selective). Keeps build output
# byte-comparable and makes broken out-of-book links easier to grep
# for during verification.
def self.rewrite_body(body, parent_url, url_to_anchor)
#
# `baseurl` is the normalised `site.config["baseurl"]`; when CI runs
# `jekyll build --baseurl /<repo>` the `relative_url`-emitted hrefs
# carry that prefix and must be stripped before the lookup, since
# `url_to_anchor` keys come from `page.url` (baseurl-less).
def self.rewrite_body(body, parent_url, url_to_anchor, baseurl)
body.gsub(/href="([^"]*)"/) do |whole_match|
href = Regexp.last_match(1)
next whole_match if EXTERNAL_PREFIXES.any? { |pfx| href.start_with?(pfx) }
Expand All @@ -211,11 +238,18 @@ def self.rewrite_body(body, parent_url, url_to_anchor)
next whole_match unless abs && abs.start_with?("/")

path_part, frag_part = abs.split("#", 2)
target = url_to_anchor[path_part]
lookup_path = strip_baseurl(path_part, baseurl)
target = url_to_anchor[lookup_path]
if target
frag_part ? %(href="##{target}-#{frag_part}") : %(href="##{target}")
else
%(href="#{abs}")
# Out-of-book target: emit the baseurl-stripped form so the
# URL the PDF reader displays is stable across local builds
# and the `--baseurl /<repo>` CI deploy path. Dead in the PDF
# either way, but the canonical (baseurl-less) form is what
# matches the live site URL when read offline.
miss_path = frag_part ? "#{lookup_path}##{frag_part}" : lookup_path
%(href="#{miss_path}")
end
end
end
Expand All @@ -227,6 +261,7 @@ def self.process(page)
parent_map = build_anchor_to_parent(site)
return if parent_map.empty?
landing_anchors = build_landing_anchors(site)
baseurl = normalize_baseurl(site.config["baseurl"])

start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)

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

parent_url = parent_map[anchor_id]
if parent_url
new_body = rewrite_body(body, parent_url, url_to_anchor)
new_body = rewrite_body(body, parent_url, url_to_anchor, baseurl)
rewritten += 1 if new_body != body
body = new_body
end
Expand Down
136 changes: 114 additions & 22 deletions docs/_plugins/jekyll-relative-links-patch.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# frozen_string_literal: true

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

require "jekyll-relative-links"

Expand All @@ -70,9 +105,66 @@ def potential_targets_by_path
end
end

# Pages indexed by their rendered URL (permalink), leading slash
# stripped to match the form `path_from_root` produces. Folder-
# style permalinks (URL ending in `/`) are also indexed under
# their trimmed form so source markdown can drop the trailing
# slash. Restricted to pages and writable docs -- static files
# have a `url` but it's just the file path, which the by_path
# table already covers.
#
# `JekyllRedirectFrom::RedirectPage` instances are excluded:
# the jekyll-redirect-from plugin synthesizes a stub page for
# every `redirect_from` alias, each with `url` equal to the
# alias itself. Indexing those would route source links through
# the redirect stub (a one-hop intermediate that only works in
# a browser) instead of resolving straight to the canonical
# target. The `by_redirect_from` table below indexes the same
# aliases but points at the canonical page, which is what we
# want.
def potential_targets_by_url
@potential_targets_by_url ||= begin
is_redirect_stub = defined?(JekyllRedirectFrom::RedirectPage) \
? ->(p) { p.is_a?(JekyllRedirectFrom::RedirectPage) } \
: ->(_p) { false }
(site.pages + site.docs_to_write).each_with_object({}) do |p, h|
next if is_redirect_stub.call(p)
url = p.url.to_s
next if url.empty? || url == "/"
key = url.sub(%r!\A/!, "")
h[key] = p unless h.key?(key)
if key.end_with?("/")
alt = key.chomp("/")
h[alt] = p unless h.key?(alt)
end
end
end
end

# Pages indexed by their `redirect_from` aliases (set by the
# jekyll-redirect-from plugin). Each alias is normalised to the
# leading-slash-stripped, trailing-slash-trimmed form so source
# markdown using a historical URL (e.g. a moved page's old slug)
# resolves to the page's current canonical URL.
def potential_targets_by_redirect_from
@potential_targets_by_redirect_from ||= begin
(site.pages + site.docs_to_write).each_with_object({}) do |p, h|
Array(p.data["redirect_from"]).each do |alias_url|
alias_str = alias_url.to_s
next if alias_str.empty?
key = alias_str.sub(%r!\A/!, "").chomp("/")
next if key.empty?
h[key] = p unless h.key?(key)
end
end
end
end

def url_for_path(path)
path = CGI.unescape(path)
target = potential_targets_by_path[path]
target = potential_targets_by_path[path] ||
potential_targets_by_url[path.chomp("/")] ||
potential_targets_by_redirect_from[path.chomp("/")]
relative_url(target.url) if target&.url
end
end
Expand Down
Loading
Loading