From 8e49aefcdae4242e4d785a34ee14f4d4d4bc4c1e Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 11 Jun 2026 10:25:37 +0200 Subject: [PATCH 1/3] type stability for less invalidations --- src/Hyperscript.jl | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Hyperscript.jl b/src/Hyperscript.jl index 0ce9d13..384385d 100644 --- a/src/Hyperscript.jl +++ b/src/Hyperscript.jl @@ -180,7 +180,24 @@ printescaped(io::IO, x::AbstractChar, escapes) = printescaped(io, string(x), esc printescaped(io::IO, x, escapes) = printescaped(io, sprint(print, x), escapes) # pass numbers (and 1-character attributes) through untrammelled -kebab(camel::String) = length(camel) > 1 ? join(islowercase(c) || isnumeric(c) || c == '-' ? c : '-' * lowercase(c) for c in camel) : camel +function kebab(camel::String) + length(camel) > 1 || return camel + # explicit loop instead of `join` over a generator: the generator join + # path carries abstract-iteration edges (`>(::Int, ::Any)` deep in + # Base.collect) that get invalidated by half the ecosystem (e.g. SIMD.jl + # comparison methods), recompiling every DOM-construction caller; the + # loop is concretely inferred end to end + io = IOBuffer(sizehint = ncodeunits(camel) + 8) + for c in camel + if islowercase(c) || isnumeric(c) || c == '-' + print(io, c) + else + print(io, '-') + print(io, lowercase(c)) + end + end + return String(take!(io)) +end ## HTMLSVG From 549ddbb73c4a16109c140ba373ead6d3ec71bdfa Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 11 Jun 2026 10:55:50 +0200 Subject: [PATCH 2/3] add CI --- .github/workflows/CI.yml | 28 ++++++++++++++++++++++++ .travis.yml | 35 ------------------------------ appveyor.yml | 47 ---------------------------------------- 3 files changed, 28 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/CI.yml delete mode 100644 .travis.yml delete mode 100644 appveyor.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..7eaca09 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,28 @@ +name: CI +on: + push: + branches: + - master + tags: ['*'] + pull_request: + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} +jobs: + test: + name: Julia 1 - ubuntu-latest + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: '1' + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v4 + with: + files: lcov.info diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 31ca4a3..0000000 --- a/.travis.yml +++ /dev/null @@ -1,35 +0,0 @@ -## Documentation: http://docs.travis-ci.com/user/languages/julia/ -language: julia -os: - - linux - - osx -julia: - - 0.7 - - nightly -notifications: - email: false -git: - depth: 99999999 - -## uncomment the following lines to allow failures on nightly julia -## (tests will run but not make your overall status red) -#matrix: -# allow_failures: -# - julia: nightly - -## uncomment and modify the following lines to manually install system packages -#addons: -# apt: # apt-get for linux -# packages: -# - gfortran -#before_script: # homebrew for mac -# - if [ $TRAVIS_OS_NAME = osx ]; then brew install gcc; fi - -## uncomment the following lines to override the default test script -#script: -# - julia -e 'Pkg.clone(pwd()); Pkg.build("Hyperscript"); Pkg.test("Hyperscript"; coverage=true)' -after_success: - # push coverage results to Coveralls - - julia -e 'cd(Pkg.dir("Hyperscript")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(Coveralls.process_folder())' - # push coverage results to Codecov - - julia -e 'cd(Pkg.dir("Hyperscript")); Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder())' diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 937c877..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,47 +0,0 @@ -environment: - matrix: - - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6-latest-win32.exe" - - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe" - - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe" - - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe" - -## uncomment the following lines to allow failures on nightly julia -## (tests will run but not make your overall status red) -#matrix: -# allow_failures: -# - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x86/julia-latest-win32.exe" -# - JULIA_URL: "https://julialangnightlies-s3.julialang.org/bin/winnt/x64/julia-latest-win64.exe" - -branches: - only: - - master - - /release-.*/ - -notifications: - - provider: Email - on_build_success: false - on_build_failure: false - on_build_status_changed: false - -install: - - ps: "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12" -# If there's a newer build queued for the same PR, cancel this one - - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` - https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` - Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` - throw "There are newer queued builds for this pull request, failing early." } -# Download most recent Julia Windows binary - - ps: (new-object net.webclient).DownloadFile( - $env:JULIA_URL, - "C:\projects\julia-binary.exe") -# Run installer silently, output to C:\projects\julia - - C:\projects\julia-binary.exe /S /D=C:\projects\julia - -build_script: -# Need to convert from shallow to complete for Pkg.clone to work - - IF EXIST .git\shallow (git fetch --unshallow) - - C:\projects\julia\bin\julia -e "versioninfo(); - Pkg.clone(pwd(), \"Hyperscript\"); Pkg.build(\"Hyperscript\")" - -test_script: - - C:\projects\julia\bin\julia -e "Pkg.test(\"Hyperscript\")" From 37b34888f7077324ded6818432768812ee156299 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 11 Jun 2026 11:27:38 +0200 Subject: [PATCH 3/3] performance + correctness fixes --- src/Hyperscript.jl | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Hyperscript.jl b/src/Hyperscript.jl index 384385d..44faa6f 100644 --- a/src/Hyperscript.jl +++ b/src/Hyperscript.jl @@ -254,9 +254,18 @@ renderdomchild(io, rctx::RenderContext, ctx::HTMLSVG, node::AbstractNode{HTMLSVG renderdomchild(io, rctx::RenderContext, ctx, x::Nothing) = nothing # Render and escape other HTMLSVG children, including CSS nodes, in the parent context. -# If a child is `showable` with text/html, render with that using `repr`. -renderdomchild(io, rctx::RenderContext, ctx, x) = - showable(MIME("text/html"), x) ? show(io, MIME("text/html"), x) : printescaped(io, x, escapechild(ctx)) +# If a child is `showable` with text/html, render with that using `show`. +# Fast paths for common child types avoid the `showable` check, which is slow +# since it calls `hasmethod`. +function renderdomchild(io, rctx::RenderContext, ctx, x) + if x isa Number || x isa AbstractString + printescaped(io, x, escapechild(ctx)) + elseif x isa HTML || showable(MIME("text/html"), x) + show(io, MIME("text/html"), x) + else + printescaped(io, x, escapechild(ctx)) + end +end # All camelCase attribute names from HTML 4, HTML 5, SVG 1.1, SVG Tiny 1.2, and SVG 2 const HTML_SVG_CAMELS = Dict(lowercase(x) => x for x in [ @@ -480,16 +489,15 @@ struct Style augmentcss(id, node) = Node{CSS}( context(node), isempty(attrs(node)) || ismedia(node) ? tag(node) : tag(node) * "[v-style$id]", - augmentcss.(id, children(node)), + Any[augmentcss(id, child) for child in children(node)], attrs(node) ) Style(id::Int, styles) = new(id, [augmentcss(id, node) for node in styles]) end -style_id = 0 +const STYLE_ID = Threads.Atomic{Int}(0) function Style(styles...) - global style_id - Style(style_id += 1, styles) + Style(Threads.atomic_add!(STYLE_ID, 1) + 1, styles) end styles(x::Style) = x.styles @@ -503,7 +511,7 @@ augmentdom(id, x::Styled) = x # `Styled` nodes act as cascade barriers augmentdom(id, node::Node{T}) where {T} = Node{T}( context(node), tag(node), - augmentdom.(id, children(node)), + Any[augmentdom(id, child) for child in children(node)], push!(copy(attrs(node)), "v-style$id" => nothing) # note: makes a defensive copy ) (s::Style)(x::Node) = Styled(augmentdom(s.id, x), s)