From 7abbdab7fcb99f87c38775ef291fd375caef54c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 20:48:15 +0000 Subject: [PATCH 1/4] Initial plan From 06c57ac4dbfbf74eecc03199e923f2e805e5871d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 20:53:07 +0000 Subject: [PATCH 2/4] Add docs directory with demo site and GitHub Actions deployment workflow Co-authored-by: phieri <12006381+phieri@users.noreply.github.com> --- .github/workflows/deploy.yml | 37 +++ docs/callsign.css | 59 +++++ docs/callsign.js | 467 +++++++++++++++++++++++++++++++++++ docs/index.html | 34 +++ 4 files changed, 597 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 docs/callsign.css create mode 100644 docs/callsign.js create mode 100644 docs/index.html diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f950c35 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,37 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'docs' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/callsign.css b/docs/callsign.css new file mode 100644 index 0000000..190aab8 --- /dev/null +++ b/docs/callsign.css @@ -0,0 +1,59 @@ +/** + * Callsign.js Stylesheet + * Styles for rendering ITU radio call signs with visual structure + */ + +:host { + /* CSS Custom Properties for easy theming */ + --cs-border-color: #ddd; + --cs-background-color: #ddd; + --cs-border-radius: 3px; + --cs-border-width: 1px; +} + +.cs-wrapper { + display: inline; + padding: 0; + font-variant-numeric: slashed-zero; +} + +/* First child styling (usually the flag) */ +.cs-wrapper :first-child { + border: var(--cs-border-width) solid var(--cs-border-color); + border-right: none; + border-bottom-left-radius: var(--cs-border-radius); + border-top-left-radius: var(--cs-border-radius); +} + +/* Country flag */ +.cs-flag { + padding-right: 1px; + background-color: var(--cs-background-color); +} + +/* Call sign prefix */ +.cs-prefix { + padding-left: 1px; + border-bottom: var(--cs-border-width) solid var(--cs-border-color); + border-top: var(--cs-border-width) solid var(--cs-border-color); +} + +/* Call sign digit */ +.cs-digit { + border-bottom: var(--cs-border-width) solid var(--cs-border-color); + border-top: var(--cs-border-width) solid var(--cs-border-color); +} + +/* Call sign suffix */ +.cs-suffix { + padding-right: 1px; + border: var(--cs-border-width) solid var(--cs-border-color); + border-left: none; + border-bottom-right-radius: var(--cs-border-radius); + border-top-right-radius: var(--cs-border-radius); +} + +/* Monospace font option */ +.monospace { + font-family: monospace; +} diff --git a/docs/callsign.js b/docs/callsign.js new file mode 100644 index 0000000..a7acc58 --- /dev/null +++ b/docs/callsign.js @@ -0,0 +1,467 @@ +/** + * @file Highlight radio call signs (including amateur) in web pages with this JavaScript library. + * @version 1.2.2 + * @author Philip Eriksson + * @see {@link https://github.com/phieri/callsign.js|Repository at GitHub} + */ + +/** @constant */ +const PHONETIC_TABLE = new Map([ + ['A', 'Alfa'], + ['B', 'Bravo'], + ['C', 'Charlie'], + ['D', 'Delta'], + ['E', 'Echo'], + ['F', 'Foxtrot'], + ['G', 'Golf'], + ['H', 'Hotel'], + ['I', 'India'], + ['J', 'Juliett'], + ['K', 'Kilo'], + ['L', 'Lima'], + ['M', 'Mike'], + ['N', 'November'], + ['O', 'Oscar'], + ['P', 'Papa'], + ['Q', 'Quebec'], + ['R', 'Romeo'], + ['S', 'Sierra'], + ['T', 'Tango'], + ['U', 'Uniform'], + ['V', 'Victor'], + ['W', 'Whiskey'], + ['X', 'X-ray'], + ['Y', 'Yankee'], + ['Z', 'Zulu'], + ['0', 'Ziro'], + ['1', 'One'], + ['2', 'Two'], + ['3', 'Tree'], + ['4', 'Four'], + ['5', 'Five'], + ['6', 'Six'], + ['7', 'Seven'], + ['8', 'Eight'], + ['9', 'Niner'], +]); + +/** @constant */ +const PREFIX_TABLE = new Map([ + ['AD', ['C3']], + ['AE', ['A6']], + ['AF', ['YA', 'T6']], + ['AG', ['V2']], + ['AL', ['ZA']], + ['AO', ['D2', 'D3']], + ['AR', ['AY', 'AZ', 'LO', 'LP', 'LQ', 'LR', 'LS', 'LT', 'LU', 'LV', 'LW']], + ['AT', ['OE']], + ['AU', ['AX', 'VH', 'VI', 'VJ', 'VK', 'VN', 'VZ']], + ['BA', ['E7', 'T9']], + ['BB', ['8P']], + ['BD', ['S2', 'S3']], + ['BE', ['ON', 'OO', 'OP', 'OQ', 'OR', 'OS', 'OT']], + ['BF', ['XT']], + ['BG', ['LZ']], + ['BH', ['A9']], + ['BO', ['CP']], + ['BR', ['PP', 'PQ', 'PR', 'PS', 'PT', 'PU', 'PV', 'PW', 'PX', 'PY', 'ZV', 'ZW', 'ZX', 'ZY', 'ZZ']], + ['BS', ['C6']], + ['BT', ['A5']], + ['BW', ['A2']], + ['BY', ['EU', 'EV', 'EW']], + ['BZ', ['V3']], + ['CA', ['CF', 'CG', 'CH', 'CI', 'CJ', 'CK', 'CY', 'CZ', 'VA', 'VB', 'VC', 'VD', 'VE', 'VF', 'VG', 'VO', 'VX', 'VY', 'XJ', 'XK', 'XL', 'XM', 'XN', 'XO']], + ['CD', ['9Q']], + ['CF', ['TL']], + ['CG', ['TN']], + ['CH', ['HB', 'HE']], + ['CI', ['TU']], + ['CL', ['CA', 'CB', 'CC', 'CD', 'CE', 'XQ', 'XR', '3G']], + ['CM', ['TJ']], + ['CN', ['B', 'VR', 'XS', 'XX']], + ['CO', ['HJ', 'HK', '5J', '5K']], + ['CR', ['TE', 'TI']], + ['CU', ['CM', 'CO', 'T4']], + ['CY', ['5B', 'C4', 'H2', 'P3']], + ['CZ', ['OK', 'OL']], + ['DE', ['DA', 'DB', 'DC', 'DD', 'DE', 'DF', 'DG', 'DH', 'DI', 'DJ', 'DK', 'DL', 'DM', 'DN', 'DO', 'DP', 'DQ', 'DR']], + ['DK', ['OU', 'OV', 'OW', 'OX', 'OY', 'OZ', 'XP']], + ['DM', ['J7']], + ['DO', ['HI']], + ['DZ', ['7X']], + ['EC', ['HC', 'HD', '5X']], + ['EE', ['ES']], + ['EG', ['SU']], + ['ES', ['AM', 'AN', 'AO', 'EA', 'EB', 'EC', 'ED', 'EE', 'EF', 'EG', 'EH']], + ['ET', ['ET']], + ['FI', ['OF', 'OG', 'OH', 'OI', 'OJ']], + ['FR', ['F', 'HW', 'HX', 'HY', 'TH', 'TM', 'TN', 'TO', 'TP', 'TQ', 'TR', 'TS', 'TT', 'TU', 'TV', 'TW', 'TX', 'TY', 'TZ']], + ['GA', ['TR']], + ['GB', ['G', 'M', 'VP', 'VQ', 'VS', 'ZB', 'ZC', 'ZD', 'ZE', 'ZF', 'ZG', 'ZH', 'ZI', 'ZJ', 'ZN', 'ZO', 'ZQ']], + ['GD', ['J3']], + ['GF', ['FY']], + ['GH', ['9G']], + ['GQ', ['3C']], + ['GR', ['J4', 'SV', 'SW', 'SX', 'SY', 'SZ']], + ['GT', ['TD', 'TG']], + ['GY', ['8R']], + ['HK', ['VR']], + ['HN', ['HQ', 'HR']], + ['HR', ['9A']], + ['HT', ['4V', 'HH']], + ['HU', ['HA', 'HG']], + ['ID', ['YB', 'YC', 'YD', 'YE', 'YF', 'YG', 'YH', '7A', '7B', '7C', '7D', '7E', '7F', '7G', '7H', '7I', '8A', '8B', '8C', '8D', '8E', '8F', '8G', '8H', '8I']], + ['IE', ['EI', 'EJ']], + ['IL', ['4X', '4Z']], + ['IN', ['AT', 'AU', 'AV', 'AW', 'VT', 'VU', 'VV', 'VW']], + ['IQ', ['HN', 'YI']], + ['IR', ['EP', 'EQ']], + ['IS', ['TF']], + ['IT', ['I', 'IZ']], + ['JM', ['6Y']], + ['JO', ['JY']], + ['JP', ['JA', 'JB', 'JC', 'JD', 'JE', 'JF', 'JG', 'JH', 'JI', 'JJ', 'JK', 'JL', 'JM', 'JN', 'JO', 'JP', 'JQ', 'JR', 'JS']], + ['KE', ['5Z']], + ['KH', ['XU']], + ['KN', ['V4']], + ['KP', ['HM', 'P5', 'P6', 'P7', 'P8', 'P9']], + ['KR', ['DS', 'DT', 'HL']], + ['KW', ['9K']], + ['LA', ['XW']], + ['LB', ['OD']], + ['LC', ['J6']], + ['LI', ['HB0']], + ['LK', ['4P', '4Q', '4R', '4S']], + ['LS', ['7P']], + ['LT', ['LY']], + ['LU', ['LX']], + ['LV', ['YL']], + ['LY', ['5A']], + ['MA', ['CN']], + ['MC', ['3A']], + ['MD', ['ER']], + ['ME', ['4O']], + ['MG', ['5R', '5S']], + ['MK', ['Z3']], + ['ML', ['TZ']], + ['MM', ['XY', 'XZ']], + ['MN', ['JT', 'JU', 'JV']], + ['MO', ['XX9']], + ['MT', ['9H']], + ['MU', ['3B']], + ['MW', ['7Q']], + ['MX', ['XA', 'XB', 'XC', 'XD', 'XE', 'XF', 'XG', 'XH', 'XI', 'XJ', 'XK', 'XL', 'XM', 'XN', 'XO', '4A', '4B', '4C', '6D', '6E', '6F', '6G', '6H', '6I', '6J']], + ['MY', ['9M']], + ['MZ', ['C9']], + ['NA', ['V5']], + ['NE', ['5U']], + ['NG', ['5N']], + ['NI', ['H6', 'H7', 'HT']], + ['NL', ['PA', 'PB', 'PC', 'PD', 'PE', 'PF', 'PG', 'PH', 'PI', 'PJ']], + ['NO', ['LA', 'LB', 'LC', 'LD', 'LE', 'LF', 'LG', 'LH', 'LI', 'LJ', 'LK', 'LL', 'LM', 'LN']], + ['NP', ['9N']], + ['NZ', ['ZK', 'ZL', 'ZM']], + ['OM', ['A4']], + ['PA', ['HO', 'HP', '3E', '3F']], + ['PE', ['OA', 'OB', 'OC', '4T']], + ['PH', ['DU', 'DV', 'DW', 'DX', 'DY', 'DZ', '4D', '4E', '4F', '4G', '4H', '4I']], + ['PK', ['AP', 'AQ', 'AR', 'AS', '6P', '6Q', '6R', '6S']], + ['PL', ['HF', 'SN', 'SO', 'SP', 'SQ', 'SR', '3Z']], + ['PR', ['KP', 'NP', 'WP']], + ['PT', ['CR', 'CS', 'CT', 'CU']], + ['PY', ['ZP']], + ['QA', ['A7']], + ['RE', ['FR']], + ['RO', ['YO', 'YP', 'YQ', 'YR']], + ['RS', ['YT', 'YU']], + ['RU', ['R', 'UA', 'UB', 'UC', 'UD', 'UE', 'UF', 'UG', 'UH', 'UI']], + ['SA', ['HZ', '7Z', '8Z']], + ['SC', ['S7', 'S79']], + ['SE', ['SA', 'SB', 'SC', 'SD', 'SE', 'SF', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', '7S']], + ['SG', ['9V']], + ['SI', ['S5']], + ['SK', ['OM']], + ['SM', ['T7']], + ['SN', ['6V', '6W']], + ['SR', ['PZ']], + ['SV', ['HU', 'YS']], + ['SY', ['YK']], + ['SZ', ['3D']], + ['TD', ['TT']], + ['TH', ['HS']], + ['TN', ['3V']], + ['TR', ['TA', 'TB', 'TC', 'YM']], + ['TT', ['9Y', '9Z']], + ['TW', ['BM', 'BN', 'BO', 'BP', 'BQ', 'BU', 'BV', 'BW', 'BX']], + ['TZ', ['5H', '5I']], + ['UA', ['EM', 'EN', 'EO', 'UR', 'US', 'UT', 'UU', 'UV', 'UW', 'UX', 'UY', 'UZ']], + ['UG', ['5X']], + ['US', ['AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'K', 'N', 'W']], + ['UY', ['CV', 'CW', 'CX']], + ['VA', ['HV']], + ['VC', ['J8']], + ['VE', ['4M', 'YV', 'YW', 'YX', 'YY']], + ['VN', ['3W', 'XV']], + ['VU', ['YJ']], + ['YE', ['7O']], + ['ZA', ['ZR', 'ZS', 'ZT', 'ZU']], + ['ZM', ['9J']], + ['ZW', ['Z2']], +]); + +/** @constant */ +const SEARCH_REGEX = /([A-Z,\d]{1,3}\d[A-Z]{1,3}(?:\/\d)?)\s/; + +/** @constant */ +const PARTS_REGEX = /([A-Z,\d]{1,3})(\d)([A-Z]{1,3})(?:\/(\d))?/; + +/** @constant */ +const DEFAULT_CSS_PATH = 'callsign.css'; + +// Cache script element and configuration +let scriptElement = null; +let config = null; + +/** + * Gets the script element and caches it + * @returns {HTMLScriptElement|null} + */ +function getScriptElement() { + if (!scriptElement) { + scriptElement = document.getElementById('callsign-js'); + } + return scriptElement; +} + +/** + * Gets configuration from script element dataset + * @returns {Object} + */ +function getConfig() { + if (!config) { + const script = getScriptElement(); + if (!script) { + console.warn('callsign.js: Script element with id="callsign-js" not found'); + return { + flag: 'true', + monospace: 'true', + phonetic: 'true', + search: 'false', + cssPath: DEFAULT_CSS_PATH + }; + } + config = { + flag: script.dataset.flag || 'true', + monospace: script.dataset.monospace || 'true', + phonetic: script.dataset.phonetic || 'true', + search: script.dataset.search || 'false', + cssPath: script.dataset.cssPath || DEFAULT_CSS_PATH + }; + } + return config; +} + +/** + * Custom element for rendering radio call signs with country flags and phonetic information + * @extends HTMLElement + */ +class Callsign extends HTMLElement { + constructor() { + super(); + + const configuration = getConfig(); + const callsignText = this.innerHTML.trim(); + + // Validate call sign format + const match = callsignText.match(PARTS_REGEX); + if (!match) { + console.warn(`callsign.js: Invalid call sign format: ${callsignText}`); + return; + } + + const shadow = this.attachShadow({ + mode: 'open' + }); + + const wrapper = document.createElement('span'); + wrapper.classList.add('cs-wrapper'); + if (configuration.monospace !== 'false') { + wrapper.classList.add('monospace'); + } + + const parts = new Map([ + ['prefix', match[1]], + ['digit', match[2]], + ['suffix', match[3]], + ]); + + // Add phonetic information + if (configuration.phonetic !== 'false') { + const phonetic = Callsign.getPhonetics(match[0]); + wrapper.setAttribute('aria-label', phonetic); + wrapper.setAttribute('title', phonetic); + } + + // Add country flag + if (configuration.flag !== 'false') { + const flagElement = this.createFlagElement(parts.get('prefix')); + if (flagElement) { + wrapper.appendChild(flagElement); + } + } + + // Add call sign parts + for (const [key, value] of parts) { + const partElement = document.createElement('span'); + partElement.textContent = value; + partElement.className = `cs-${key}`; + if (configuration.phonetic !== 'false') { + partElement.setAttribute('aria-hidden', 'true'); + } + wrapper.appendChild(partElement); + } + + // Add stylesheet + const linkElement = document.createElement('link'); + linkElement.setAttribute('rel', 'stylesheet'); + linkElement.setAttribute('href', configuration.cssPath); + shadow.appendChild(linkElement); + + shadow.appendChild(wrapper); + } + + /** + * Creates a flag element for the given prefix + * @param {string} prefix - The call sign prefix + * @returns {HTMLSpanElement|null} + */ + createFlagElement(prefix) { + for (const [iso, prefixes] of PREFIX_TABLE) { + if (prefixes.includes(prefix)) { + const flagElement = document.createElement('span'); + flagElement.className = 'cs-flag'; + flagElement.title = iso; + flagElement.textContent = Callsign.getFlag(iso); + return flagElement; + } + } + return null; + } + + /** + * Converts an ISO country code to a Unicode Regional Indicator Symbol (emoji flag). + * @param {!string} code The ISO 3166-1 alpha-2 code + * @returns {string} + */ + static getFlag(code) { + return String.fromCodePoint(...[...code].map(c => c.charCodeAt() + 127397)); + } + + /** + * @param {string} letters The string of letters to expand + * @returns {string} + */ + static getPhonetics(letters) { + let ret = ''; + for (let i = 0; i < letters.length; i++) { + const phonetic = PHONETIC_TABLE.get(letters.charAt(i)); + if (phonetic) { + ret += `${phonetic} `; + } + } + return ret.slice(0, -1); + } + + /** + * Goes through the entire webpage and adds markup to untagged call signs. + * Uses TreeWalker to safely traverse text nodes without modifying innerHTML. + */ + static searchCallsigns() { + // Create a TreeWalker to find all text nodes + const walker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_TEXT, + { + acceptNode(node) { + // Skip script and style elements + const parent = node.parentElement; + if (!parent || parent.tagName === 'SCRIPT' || + parent.tagName === 'STYLE' || + parent.tagName === 'CALL-SIGN') { + return NodeFilter.FILTER_REJECT; + } + // Only accept nodes with potential call signs + if (SEARCH_REGEX.test(node.textContent)) { + return NodeFilter.FILTER_ACCEPT; + } + return NodeFilter.FILTER_REJECT; + } + } + ); + + const nodesToReplace = []; + let currentNode; + + // Collect nodes to replace (can't modify while walking) + while ((currentNode = walker.nextNode())) { + nodesToReplace.push(currentNode); + } + + // Process each text node + for (const node of nodesToReplace) { + const text = node.textContent; + const matches = []; + let match; + let lastIndex = 0; + const regex = new RegExp(SEARCH_REGEX, 'g'); + + while ((match = regex.exec(`${text} `)) !== null) { + matches.push({ + callsign: match[1], + index: match.index, + length: match[1].length + }); + } + + if (matches.length > 0) { + const parent = node.parentNode; + const fragment = document.createDocumentFragment(); + lastIndex = 0; + + for (const matchInfo of matches) { + // Add text before the call sign + if (matchInfo.index > lastIndex) { + fragment.appendChild( + document.createTextNode(text.substring(lastIndex, matchInfo.index)) + ); + } + + // Create call-sign element + const callsignElement = document.createElement('call-sign'); + callsignElement.textContent = matchInfo.callsign; + fragment.appendChild(callsignElement); + + lastIndex = matchInfo.index + matchInfo.length; + } + + // Add remaining text + if (lastIndex < text.length) { + fragment.appendChild(document.createTextNode(text.substring(lastIndex))); + } + + parent.replaceChild(fragment, node); + } + } + } +} + +// Initialize when DOM is ready +if (getConfig().search !== 'false') { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => Callsign.searchCallsigns()); + } else { + Callsign.searchCallsigns(); + } +} + +customElements.define('call-sign', Callsign); diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..5c162d5 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,34 @@ + + + + + + callsign.js example page + + + + + + + + +
+

callsign.js example page

+
+
+ +
+ + + + From b5e4ee3b7074de1291dcfd025f2e247759966da0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 20:54:06 +0000 Subject: [PATCH 3/4] Add demo link to README Co-authored-by: phieri <12006381+phieri@users.noreply.github.com> --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 28d3450..7bbebf8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # callsign.js This JavaScript library gives website users more ways to interact with written [ITU](https://www.itu.int/en/) call signs, including for amateur radio. +## Demo +See a live example at [https://phieri.github.io/callsign.js/](https://phieri.github.io/callsign.js/) + # Usage Upload callsign.js and callsign.css to the webserver and add callsign.js to the `` section. ```html From 5985f1668001c38405a514bb6e8321db584ec5f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 21:20:32 +0000 Subject: [PATCH 4/4] Make deployment automatic by copying files from src/ during workflow Co-authored-by: phieri <12006381+phieri@users.noreply.github.com> --- .github/workflows/deploy.yml | 5 + .gitignore | 2 + docs/callsign.css | 59 ----- docs/callsign.js | 467 ----------------------------------- 4 files changed, 7 insertions(+), 526 deletions(-) delete mode 100644 docs/callsign.css delete mode 100644 docs/callsign.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f950c35..8e2bbe8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,6 +24,11 @@ jobs: - name: Checkout uses: actions/checkout@v5 + - name: Prepare deployment directory + run: | + cp src/callsign.js docs/ + cp src/callsign.css docs/ + - name: Setup Pages uses: actions/configure-pages@v5 diff --git a/.gitignore b/.gitignore index af54ec3..5ffc223 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ node_modules/ package-lock.json test-page.html test.html +docs/callsign.js +docs/callsign.css diff --git a/docs/callsign.css b/docs/callsign.css deleted file mode 100644 index 190aab8..0000000 --- a/docs/callsign.css +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Callsign.js Stylesheet - * Styles for rendering ITU radio call signs with visual structure - */ - -:host { - /* CSS Custom Properties for easy theming */ - --cs-border-color: #ddd; - --cs-background-color: #ddd; - --cs-border-radius: 3px; - --cs-border-width: 1px; -} - -.cs-wrapper { - display: inline; - padding: 0; - font-variant-numeric: slashed-zero; -} - -/* First child styling (usually the flag) */ -.cs-wrapper :first-child { - border: var(--cs-border-width) solid var(--cs-border-color); - border-right: none; - border-bottom-left-radius: var(--cs-border-radius); - border-top-left-radius: var(--cs-border-radius); -} - -/* Country flag */ -.cs-flag { - padding-right: 1px; - background-color: var(--cs-background-color); -} - -/* Call sign prefix */ -.cs-prefix { - padding-left: 1px; - border-bottom: var(--cs-border-width) solid var(--cs-border-color); - border-top: var(--cs-border-width) solid var(--cs-border-color); -} - -/* Call sign digit */ -.cs-digit { - border-bottom: var(--cs-border-width) solid var(--cs-border-color); - border-top: var(--cs-border-width) solid var(--cs-border-color); -} - -/* Call sign suffix */ -.cs-suffix { - padding-right: 1px; - border: var(--cs-border-width) solid var(--cs-border-color); - border-left: none; - border-bottom-right-radius: var(--cs-border-radius); - border-top-right-radius: var(--cs-border-radius); -} - -/* Monospace font option */ -.monospace { - font-family: monospace; -} diff --git a/docs/callsign.js b/docs/callsign.js deleted file mode 100644 index a7acc58..0000000 --- a/docs/callsign.js +++ /dev/null @@ -1,467 +0,0 @@ -/** - * @file Highlight radio call signs (including amateur) in web pages with this JavaScript library. - * @version 1.2.2 - * @author Philip Eriksson - * @see {@link https://github.com/phieri/callsign.js|Repository at GitHub} - */ - -/** @constant */ -const PHONETIC_TABLE = new Map([ - ['A', 'Alfa'], - ['B', 'Bravo'], - ['C', 'Charlie'], - ['D', 'Delta'], - ['E', 'Echo'], - ['F', 'Foxtrot'], - ['G', 'Golf'], - ['H', 'Hotel'], - ['I', 'India'], - ['J', 'Juliett'], - ['K', 'Kilo'], - ['L', 'Lima'], - ['M', 'Mike'], - ['N', 'November'], - ['O', 'Oscar'], - ['P', 'Papa'], - ['Q', 'Quebec'], - ['R', 'Romeo'], - ['S', 'Sierra'], - ['T', 'Tango'], - ['U', 'Uniform'], - ['V', 'Victor'], - ['W', 'Whiskey'], - ['X', 'X-ray'], - ['Y', 'Yankee'], - ['Z', 'Zulu'], - ['0', 'Ziro'], - ['1', 'One'], - ['2', 'Two'], - ['3', 'Tree'], - ['4', 'Four'], - ['5', 'Five'], - ['6', 'Six'], - ['7', 'Seven'], - ['8', 'Eight'], - ['9', 'Niner'], -]); - -/** @constant */ -const PREFIX_TABLE = new Map([ - ['AD', ['C3']], - ['AE', ['A6']], - ['AF', ['YA', 'T6']], - ['AG', ['V2']], - ['AL', ['ZA']], - ['AO', ['D2', 'D3']], - ['AR', ['AY', 'AZ', 'LO', 'LP', 'LQ', 'LR', 'LS', 'LT', 'LU', 'LV', 'LW']], - ['AT', ['OE']], - ['AU', ['AX', 'VH', 'VI', 'VJ', 'VK', 'VN', 'VZ']], - ['BA', ['E7', 'T9']], - ['BB', ['8P']], - ['BD', ['S2', 'S3']], - ['BE', ['ON', 'OO', 'OP', 'OQ', 'OR', 'OS', 'OT']], - ['BF', ['XT']], - ['BG', ['LZ']], - ['BH', ['A9']], - ['BO', ['CP']], - ['BR', ['PP', 'PQ', 'PR', 'PS', 'PT', 'PU', 'PV', 'PW', 'PX', 'PY', 'ZV', 'ZW', 'ZX', 'ZY', 'ZZ']], - ['BS', ['C6']], - ['BT', ['A5']], - ['BW', ['A2']], - ['BY', ['EU', 'EV', 'EW']], - ['BZ', ['V3']], - ['CA', ['CF', 'CG', 'CH', 'CI', 'CJ', 'CK', 'CY', 'CZ', 'VA', 'VB', 'VC', 'VD', 'VE', 'VF', 'VG', 'VO', 'VX', 'VY', 'XJ', 'XK', 'XL', 'XM', 'XN', 'XO']], - ['CD', ['9Q']], - ['CF', ['TL']], - ['CG', ['TN']], - ['CH', ['HB', 'HE']], - ['CI', ['TU']], - ['CL', ['CA', 'CB', 'CC', 'CD', 'CE', 'XQ', 'XR', '3G']], - ['CM', ['TJ']], - ['CN', ['B', 'VR', 'XS', 'XX']], - ['CO', ['HJ', 'HK', '5J', '5K']], - ['CR', ['TE', 'TI']], - ['CU', ['CM', 'CO', 'T4']], - ['CY', ['5B', 'C4', 'H2', 'P3']], - ['CZ', ['OK', 'OL']], - ['DE', ['DA', 'DB', 'DC', 'DD', 'DE', 'DF', 'DG', 'DH', 'DI', 'DJ', 'DK', 'DL', 'DM', 'DN', 'DO', 'DP', 'DQ', 'DR']], - ['DK', ['OU', 'OV', 'OW', 'OX', 'OY', 'OZ', 'XP']], - ['DM', ['J7']], - ['DO', ['HI']], - ['DZ', ['7X']], - ['EC', ['HC', 'HD', '5X']], - ['EE', ['ES']], - ['EG', ['SU']], - ['ES', ['AM', 'AN', 'AO', 'EA', 'EB', 'EC', 'ED', 'EE', 'EF', 'EG', 'EH']], - ['ET', ['ET']], - ['FI', ['OF', 'OG', 'OH', 'OI', 'OJ']], - ['FR', ['F', 'HW', 'HX', 'HY', 'TH', 'TM', 'TN', 'TO', 'TP', 'TQ', 'TR', 'TS', 'TT', 'TU', 'TV', 'TW', 'TX', 'TY', 'TZ']], - ['GA', ['TR']], - ['GB', ['G', 'M', 'VP', 'VQ', 'VS', 'ZB', 'ZC', 'ZD', 'ZE', 'ZF', 'ZG', 'ZH', 'ZI', 'ZJ', 'ZN', 'ZO', 'ZQ']], - ['GD', ['J3']], - ['GF', ['FY']], - ['GH', ['9G']], - ['GQ', ['3C']], - ['GR', ['J4', 'SV', 'SW', 'SX', 'SY', 'SZ']], - ['GT', ['TD', 'TG']], - ['GY', ['8R']], - ['HK', ['VR']], - ['HN', ['HQ', 'HR']], - ['HR', ['9A']], - ['HT', ['4V', 'HH']], - ['HU', ['HA', 'HG']], - ['ID', ['YB', 'YC', 'YD', 'YE', 'YF', 'YG', 'YH', '7A', '7B', '7C', '7D', '7E', '7F', '7G', '7H', '7I', '8A', '8B', '8C', '8D', '8E', '8F', '8G', '8H', '8I']], - ['IE', ['EI', 'EJ']], - ['IL', ['4X', '4Z']], - ['IN', ['AT', 'AU', 'AV', 'AW', 'VT', 'VU', 'VV', 'VW']], - ['IQ', ['HN', 'YI']], - ['IR', ['EP', 'EQ']], - ['IS', ['TF']], - ['IT', ['I', 'IZ']], - ['JM', ['6Y']], - ['JO', ['JY']], - ['JP', ['JA', 'JB', 'JC', 'JD', 'JE', 'JF', 'JG', 'JH', 'JI', 'JJ', 'JK', 'JL', 'JM', 'JN', 'JO', 'JP', 'JQ', 'JR', 'JS']], - ['KE', ['5Z']], - ['KH', ['XU']], - ['KN', ['V4']], - ['KP', ['HM', 'P5', 'P6', 'P7', 'P8', 'P9']], - ['KR', ['DS', 'DT', 'HL']], - ['KW', ['9K']], - ['LA', ['XW']], - ['LB', ['OD']], - ['LC', ['J6']], - ['LI', ['HB0']], - ['LK', ['4P', '4Q', '4R', '4S']], - ['LS', ['7P']], - ['LT', ['LY']], - ['LU', ['LX']], - ['LV', ['YL']], - ['LY', ['5A']], - ['MA', ['CN']], - ['MC', ['3A']], - ['MD', ['ER']], - ['ME', ['4O']], - ['MG', ['5R', '5S']], - ['MK', ['Z3']], - ['ML', ['TZ']], - ['MM', ['XY', 'XZ']], - ['MN', ['JT', 'JU', 'JV']], - ['MO', ['XX9']], - ['MT', ['9H']], - ['MU', ['3B']], - ['MW', ['7Q']], - ['MX', ['XA', 'XB', 'XC', 'XD', 'XE', 'XF', 'XG', 'XH', 'XI', 'XJ', 'XK', 'XL', 'XM', 'XN', 'XO', '4A', '4B', '4C', '6D', '6E', '6F', '6G', '6H', '6I', '6J']], - ['MY', ['9M']], - ['MZ', ['C9']], - ['NA', ['V5']], - ['NE', ['5U']], - ['NG', ['5N']], - ['NI', ['H6', 'H7', 'HT']], - ['NL', ['PA', 'PB', 'PC', 'PD', 'PE', 'PF', 'PG', 'PH', 'PI', 'PJ']], - ['NO', ['LA', 'LB', 'LC', 'LD', 'LE', 'LF', 'LG', 'LH', 'LI', 'LJ', 'LK', 'LL', 'LM', 'LN']], - ['NP', ['9N']], - ['NZ', ['ZK', 'ZL', 'ZM']], - ['OM', ['A4']], - ['PA', ['HO', 'HP', '3E', '3F']], - ['PE', ['OA', 'OB', 'OC', '4T']], - ['PH', ['DU', 'DV', 'DW', 'DX', 'DY', 'DZ', '4D', '4E', '4F', '4G', '4H', '4I']], - ['PK', ['AP', 'AQ', 'AR', 'AS', '6P', '6Q', '6R', '6S']], - ['PL', ['HF', 'SN', 'SO', 'SP', 'SQ', 'SR', '3Z']], - ['PR', ['KP', 'NP', 'WP']], - ['PT', ['CR', 'CS', 'CT', 'CU']], - ['PY', ['ZP']], - ['QA', ['A7']], - ['RE', ['FR']], - ['RO', ['YO', 'YP', 'YQ', 'YR']], - ['RS', ['YT', 'YU']], - ['RU', ['R', 'UA', 'UB', 'UC', 'UD', 'UE', 'UF', 'UG', 'UH', 'UI']], - ['SA', ['HZ', '7Z', '8Z']], - ['SC', ['S7', 'S79']], - ['SE', ['SA', 'SB', 'SC', 'SD', 'SE', 'SF', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', '7S']], - ['SG', ['9V']], - ['SI', ['S5']], - ['SK', ['OM']], - ['SM', ['T7']], - ['SN', ['6V', '6W']], - ['SR', ['PZ']], - ['SV', ['HU', 'YS']], - ['SY', ['YK']], - ['SZ', ['3D']], - ['TD', ['TT']], - ['TH', ['HS']], - ['TN', ['3V']], - ['TR', ['TA', 'TB', 'TC', 'YM']], - ['TT', ['9Y', '9Z']], - ['TW', ['BM', 'BN', 'BO', 'BP', 'BQ', 'BU', 'BV', 'BW', 'BX']], - ['TZ', ['5H', '5I']], - ['UA', ['EM', 'EN', 'EO', 'UR', 'US', 'UT', 'UU', 'UV', 'UW', 'UX', 'UY', 'UZ']], - ['UG', ['5X']], - ['US', ['AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'K', 'N', 'W']], - ['UY', ['CV', 'CW', 'CX']], - ['VA', ['HV']], - ['VC', ['J8']], - ['VE', ['4M', 'YV', 'YW', 'YX', 'YY']], - ['VN', ['3W', 'XV']], - ['VU', ['YJ']], - ['YE', ['7O']], - ['ZA', ['ZR', 'ZS', 'ZT', 'ZU']], - ['ZM', ['9J']], - ['ZW', ['Z2']], -]); - -/** @constant */ -const SEARCH_REGEX = /([A-Z,\d]{1,3}\d[A-Z]{1,3}(?:\/\d)?)\s/; - -/** @constant */ -const PARTS_REGEX = /([A-Z,\d]{1,3})(\d)([A-Z]{1,3})(?:\/(\d))?/; - -/** @constant */ -const DEFAULT_CSS_PATH = 'callsign.css'; - -// Cache script element and configuration -let scriptElement = null; -let config = null; - -/** - * Gets the script element and caches it - * @returns {HTMLScriptElement|null} - */ -function getScriptElement() { - if (!scriptElement) { - scriptElement = document.getElementById('callsign-js'); - } - return scriptElement; -} - -/** - * Gets configuration from script element dataset - * @returns {Object} - */ -function getConfig() { - if (!config) { - const script = getScriptElement(); - if (!script) { - console.warn('callsign.js: Script element with id="callsign-js" not found'); - return { - flag: 'true', - monospace: 'true', - phonetic: 'true', - search: 'false', - cssPath: DEFAULT_CSS_PATH - }; - } - config = { - flag: script.dataset.flag || 'true', - monospace: script.dataset.monospace || 'true', - phonetic: script.dataset.phonetic || 'true', - search: script.dataset.search || 'false', - cssPath: script.dataset.cssPath || DEFAULT_CSS_PATH - }; - } - return config; -} - -/** - * Custom element for rendering radio call signs with country flags and phonetic information - * @extends HTMLElement - */ -class Callsign extends HTMLElement { - constructor() { - super(); - - const configuration = getConfig(); - const callsignText = this.innerHTML.trim(); - - // Validate call sign format - const match = callsignText.match(PARTS_REGEX); - if (!match) { - console.warn(`callsign.js: Invalid call sign format: ${callsignText}`); - return; - } - - const shadow = this.attachShadow({ - mode: 'open' - }); - - const wrapper = document.createElement('span'); - wrapper.classList.add('cs-wrapper'); - if (configuration.monospace !== 'false') { - wrapper.classList.add('monospace'); - } - - const parts = new Map([ - ['prefix', match[1]], - ['digit', match[2]], - ['suffix', match[3]], - ]); - - // Add phonetic information - if (configuration.phonetic !== 'false') { - const phonetic = Callsign.getPhonetics(match[0]); - wrapper.setAttribute('aria-label', phonetic); - wrapper.setAttribute('title', phonetic); - } - - // Add country flag - if (configuration.flag !== 'false') { - const flagElement = this.createFlagElement(parts.get('prefix')); - if (flagElement) { - wrapper.appendChild(flagElement); - } - } - - // Add call sign parts - for (const [key, value] of parts) { - const partElement = document.createElement('span'); - partElement.textContent = value; - partElement.className = `cs-${key}`; - if (configuration.phonetic !== 'false') { - partElement.setAttribute('aria-hidden', 'true'); - } - wrapper.appendChild(partElement); - } - - // Add stylesheet - const linkElement = document.createElement('link'); - linkElement.setAttribute('rel', 'stylesheet'); - linkElement.setAttribute('href', configuration.cssPath); - shadow.appendChild(linkElement); - - shadow.appendChild(wrapper); - } - - /** - * Creates a flag element for the given prefix - * @param {string} prefix - The call sign prefix - * @returns {HTMLSpanElement|null} - */ - createFlagElement(prefix) { - for (const [iso, prefixes] of PREFIX_TABLE) { - if (prefixes.includes(prefix)) { - const flagElement = document.createElement('span'); - flagElement.className = 'cs-flag'; - flagElement.title = iso; - flagElement.textContent = Callsign.getFlag(iso); - return flagElement; - } - } - return null; - } - - /** - * Converts an ISO country code to a Unicode Regional Indicator Symbol (emoji flag). - * @param {!string} code The ISO 3166-1 alpha-2 code - * @returns {string} - */ - static getFlag(code) { - return String.fromCodePoint(...[...code].map(c => c.charCodeAt() + 127397)); - } - - /** - * @param {string} letters The string of letters to expand - * @returns {string} - */ - static getPhonetics(letters) { - let ret = ''; - for (let i = 0; i < letters.length; i++) { - const phonetic = PHONETIC_TABLE.get(letters.charAt(i)); - if (phonetic) { - ret += `${phonetic} `; - } - } - return ret.slice(0, -1); - } - - /** - * Goes through the entire webpage and adds markup to untagged call signs. - * Uses TreeWalker to safely traverse text nodes without modifying innerHTML. - */ - static searchCallsigns() { - // Create a TreeWalker to find all text nodes - const walker = document.createTreeWalker( - document.body, - NodeFilter.SHOW_TEXT, - { - acceptNode(node) { - // Skip script and style elements - const parent = node.parentElement; - if (!parent || parent.tagName === 'SCRIPT' || - parent.tagName === 'STYLE' || - parent.tagName === 'CALL-SIGN') { - return NodeFilter.FILTER_REJECT; - } - // Only accept nodes with potential call signs - if (SEARCH_REGEX.test(node.textContent)) { - return NodeFilter.FILTER_ACCEPT; - } - return NodeFilter.FILTER_REJECT; - } - } - ); - - const nodesToReplace = []; - let currentNode; - - // Collect nodes to replace (can't modify while walking) - while ((currentNode = walker.nextNode())) { - nodesToReplace.push(currentNode); - } - - // Process each text node - for (const node of nodesToReplace) { - const text = node.textContent; - const matches = []; - let match; - let lastIndex = 0; - const regex = new RegExp(SEARCH_REGEX, 'g'); - - while ((match = regex.exec(`${text} `)) !== null) { - matches.push({ - callsign: match[1], - index: match.index, - length: match[1].length - }); - } - - if (matches.length > 0) { - const parent = node.parentNode; - const fragment = document.createDocumentFragment(); - lastIndex = 0; - - for (const matchInfo of matches) { - // Add text before the call sign - if (matchInfo.index > lastIndex) { - fragment.appendChild( - document.createTextNode(text.substring(lastIndex, matchInfo.index)) - ); - } - - // Create call-sign element - const callsignElement = document.createElement('call-sign'); - callsignElement.textContent = matchInfo.callsign; - fragment.appendChild(callsignElement); - - lastIndex = matchInfo.index + matchInfo.length; - } - - // Add remaining text - if (lastIndex < text.length) { - fragment.appendChild(document.createTextNode(text.substring(lastIndex))); - } - - parent.replaceChild(fragment, node); - } - } - } -} - -// Initialize when DOM is ready -if (getConfig().search !== 'false') { - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', () => Callsign.searchCallsigns()); - } else { - Callsign.searchCallsigns(); - } -} - -customElements.define('call-sign', Callsign);