@@ -378,6 +378,7 @@ <h1>ASCII Punctuation Fixer</h1>
378378 </ button >
379379 < button id ="btnSwap " title ="Swap input and output "> Swap</ button >
380380 < button id ="btnCopy " title ="Copy output to clipboard "> Copy output</ button >
381+ < button id ="btnShare " title ="Copy a shareable link that loads the text via Base64 "> Share link</ button >
381382 < button id ="btnDownload " title ="Download output as .txt "> Download .txt</ button >
382383 </ div >
383384
@@ -423,6 +424,113 @@ <h1>ASCII Punctuation Fixer</h1>
423424 "use strict" ;
424425
425426 const $ = ( id ) => document . getElementById ( id ) ;
427+ // ---------- URL import/export (Base64URL) ----------
428+ // Load text from:
429+ // ?b64=... (base64url recommended)
430+ // #b64=... (recommended for longer text)
431+ // Aliases: text64, t
432+ // Optional flags (only applied if present): nfkc=1 inv=1 lig=1 live=1
433+ function padBase64 ( b64 ) {
434+ const mod = b64 . length % 4 ;
435+ return mod === 0 ? b64 : b64 + "=" . repeat ( 4 - mod ) ;
436+ }
437+
438+ function bytesToBinaryString ( bytes ) {
439+ let bin = "" ;
440+ const chunk = 0x8000 ;
441+ for ( let i = 0 ; i < bytes . length ; i += chunk ) {
442+ bin += String . fromCharCode ( ...bytes . subarray ( i , i + chunk ) ) ;
443+ }
444+ return bin ;
445+ }
446+
447+ function encodeUtf8ToBase64Url ( text ) {
448+ const bytes = new TextEncoder ( ) . encode ( text ) ;
449+ const b64 = btoa ( bytesToBinaryString ( bytes ) ) ;
450+ return b64 . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) . replace ( / = + $ / g, "" ) ;
451+ }
452+
453+ function decodeBase64UrlToUtf8 ( b64url ) {
454+ const normalized = padBase64 (
455+ String ( b64url ) . replace ( / - / g, "+" ) . replace ( / _ / g, "/" ) . replace ( / \s / g, "" )
456+ ) ;
457+ const binary = atob ( normalized ) ;
458+ const bytes = new Uint8Array ( binary . length ) ;
459+ for ( let i = 0 ; i < binary . length ; i ++ ) bytes [ i ] = binary . charCodeAt ( i ) ;
460+ return new TextDecoder ( "utf-8" , { fatal : false } ) . decode ( bytes ) ;
461+ }
462+
463+ function getUrlParam ( name ) {
464+ const qs = new URLSearchParams ( window . location . search ) ;
465+ const v1 = qs . get ( name ) ;
466+ if ( v1 !== null ) return v1 ;
467+
468+ const rawHash = window . location . hash . startsWith ( "#" ) ? window . location . hash . slice ( 1 ) : window . location . hash ;
469+ const hs = new URLSearchParams ( rawHash ) ;
470+ return hs . get ( name ) ;
471+ }
472+
473+ function hasUrlParam ( name ) {
474+ return getUrlParam ( name ) !== null ;
475+ }
476+
477+ function parseBoolParam ( value ) {
478+ if ( value === null ) return null ;
479+ const v = String ( value ) . trim ( ) . toLowerCase ( ) ;
480+ if ( [ "1" , "true" , "yes" , "y" , "on" ] . includes ( v ) ) return true ;
481+ if ( [ "0" , "false" , "no" , "n" , "off" ] . includes ( v ) ) return false ;
482+ return null ;
483+ }
484+
485+ function applyOptionsFromUrl ( ) {
486+ const nfkc = parseBoolParam ( getUrlParam ( "nfkc" ) ) ;
487+ const inv = parseBoolParam ( getUrlParam ( "inv" ) ) ;
488+ const lig = parseBoolParam ( getUrlParam ( "lig" ) ) ;
489+ const live = parseBoolParam ( getUrlParam ( "live" ) ) ;
490+
491+ if ( nfkc !== null ) $ ( "optNFKC" ) . checked = nfkc ;
492+ if ( inv !== null ) $ ( "optInvisible" ) . checked = inv ;
493+ if ( lig !== null ) $ ( "optLigatures" ) . checked = lig ;
494+ if ( live !== null ) $ ( "optLive" ) . checked = live ;
495+ }
496+
497+ function initFromUrl ( ) {
498+ // Apply checkbox flags first (if present).
499+ if ( hasUrlParam ( "nfkc" ) || hasUrlParam ( "inv" ) || hasUrlParam ( "lig" ) || hasUrlParam ( "live" ) ) {
500+ applyOptionsFromUrl ( ) ;
501+ }
502+
503+ const b64 = getUrlParam ( "b64" ) || getUrlParam ( "text64" ) || getUrlParam ( "t" ) ;
504+ if ( ! b64 ) return false ;
505+
506+ try {
507+ const text = decodeBase64UrlToUtf8 ( b64 ) ;
508+ $ ( "input" ) . value = text ;
509+ runConversion ( ) ;
510+ toast ( "Loaded input from URL." ) ;
511+ return true ;
512+ } catch ( err ) {
513+ console . error ( "Failed to decode Base64 from URL:" , err ) ;
514+ toast ( "Could not decode URL input." ) ;
515+ return false ;
516+ }
517+ }
518+
519+ function buildShareUrl ( text , opts ) {
520+ const base = window . location . origin + window . location . pathname ;
521+
522+ const params = new URLSearchParams ( ) ;
523+ params . set ( "b64" , encodeUtf8ToBase64Url ( text ) ) ;
524+
525+ // Persist current options, so the receiver sees the same behavior.
526+ params . set ( "nfkc" , opts . nfkc ? "1" : "0" ) ;
527+ params . set ( "inv" , opts . removeInvisible ? "1" : "0" ) ;
528+ params . set ( "lig" , opts . ligatures ? "1" : "0" ) ;
529+
530+ // Use hash to better tolerate longer payloads and avoid '+' being treated as space.
531+ return base + "#" + params . toString ( ) ;
532+ }
533+
426534
427535 // ---------- Replacement rules ----------
428536 // Goal: Convert common "smart punctuation" and friends into plain ASCII equivalents.
@@ -651,6 +759,37 @@ <h1>ASCII Punctuation Fixer</h1>
651759 }
652760 } ) ;
653761
762+ $ ( "btnShare" ) . addEventListener ( "click" , async ( ) => {
763+ const inputText = $ ( "input" ) . value ;
764+ const outputText = $ ( "output" ) . value ;
765+ const text = inputText || outputText ;
766+
767+ if ( ! text ) { toast ( "Nothing to share." ) ; return ; }
768+
769+ const opts = getOpts ( ) ;
770+ const url = buildShareUrl ( text , opts ) ;
771+
772+ try {
773+ await navigator . clipboard . writeText ( url ) ;
774+ toast ( url . length > 8000 ? "Share link copied (very long URL)." : "Share link copied." ) ;
775+ } catch {
776+ // Fallback for older browsers / restricted contexts
777+ const ta = document . createElement ( "textarea" ) ;
778+ ta . value = url ;
779+ ta . setAttribute ( "readonly" , "" ) ;
780+ ta . style . position = "fixed" ;
781+ ta . style . left = "-9999px" ;
782+ ta . style . top = "0" ;
783+ document . body . appendChild ( ta ) ;
784+ ta . select ( ) ;
785+ document . execCommand ( "copy" ) ;
786+ ta . remove ( ) ;
787+ toast ( url . length > 8000 ? "Share link copied (fallback, very long URL)." : "Share link copied (fallback)." ) ;
788+ }
789+
790+ // Update the address bar without a reload (handy for manual sharing)
791+ try { history . replaceState ( null , "" , url ) ; } catch { }
792+ } ) ;
654793 $ ( "btnDownload" ) . addEventListener ( "click" , ( ) => {
655794 const text = $ ( "output" ) . value ;
656795 if ( ! text ) { toast ( "Nothing to download." ) ; return ; }
@@ -714,6 +853,8 @@ <h1>ASCII Punctuation Fixer</h1>
714853
715854 // Initial state
716855 $ ( "diagWrap" ) . innerHTML = `<div class="muted">Paste text to see diagnostics.</div>` ;
856+ // Load input from URL (Base64URL)
857+ initFromUrl ( ) ;
717858} ) ( ) ;
718859</ script >
719860</ body >
0 commit comments