<div class="wrapper u-p-t-4 o-url-checker">
<h1 class="supersize">Förstå en länk (URL)</h1>
<p class="preamble o-url-checker__preamble">
Klistra in en länk här så delas den upp i sina byggstenar. Tanken är att göra det enklare att se
<strong>vem</strong> länken faktiskt går till och <strong>hur</strong> den är uppbyggd – vilket hjälper mot bedrägerilänkar.
</p>
<h2>Så använder du detta mot bedrägerilänkar</h2>
<ul>
<li>
Kontrollera att <strong>domän + toppdomän</strong> stämmer (t.ex. <strong>bank.se</strong>).
</li>
<li>
Var skeptisk om subdomänen innehåller <code>secure</code>, <code>login</code>, <code>verify</code> – det kan vara vilseledande.
</li>
<li>Var försiktig med väldigt långa länkar och många parametrar.</li>
<li>Var uppmärksam på specialtecken som efterliknar "vanliga" tecken.</li>
</ul>
<!-- LEFT: INPUT -->
<h2 class="u-m-b-0">1. Klistra in en länk</h2>
<section class="o-url-checker__box u-m-b-default">
<div class="o-url-checker__box__header">
<p class="u-m-b-0 o-url-checker__muted">
Exempel: <strong>https://microsoft.com/path/to/page?utm_source=x#newsletter</strong>
</p>
</div>
<div class="o-url-checker__box__body">
<div class="field-group u-m-b-0" id="urlInputFieldGroup">
<label for="urlInput" class="u-visuallyhidden">Länk</label>
<textarea id="urlInput" class="a-textarea" rows="5" placeholder="Klistra in en URL här…" autocomplete="off" autocapitalize="off" spellcheck="false"></textarea>
<div id="urlInputHelp" class="input-help" aria-live="polite"></div>
</div>
<div class="u-m-t-default o-url-checker__actions">
<button id="analyzeBtn" class="a-button a-button--peacock" type="button">
<span class="a-button__text">Analysera</span>
</button>
<button id="clearBtn" class="a-button a-button--transparent" type="button">
<span class="a-button__text">Rensa</span>
</button>
</div>
<div id="inputHint" class="u-m-t-default o-url-checker__message o-url-checker__message--warn" aria-live="polite"></div>
</div>
</section>
<section id="results" class="u-m-b-default o-url-checker__results" hidden aria-live="polite" aria-atomic="true">
<div id="emptyState" class="o-url-checker__muted">
<p class="u-m-b-0">Klistra in en URL så visas delarna här.</p>
</div>
<div class="u-m-b-4 u-p-t-2" id="overview">
<h2 class="u-m-b-0">2. Snabbkoll</h2>
<div class="o-url-checker__box o-url-checker__domain-focus u-m-b-default">
<div class="o-url-checker__box__body">
<div class="o-url-checker__domain-focus__hint u-m-b-1">
<h3 class="u-m-b-0">Detta är domänen du hamnar på om du klickar på denna länk.</h3>
Kontrollera tecken för tecken att domännamnet är exakt rätt.
</div>
<div class="o-url-checker__domain-focus__main o-url-checker__surface o-url-checker__surface--ash">
<span class="o-url-checker__domain-focus__label">Domän + toppdomän</span>
<span class="o-url-checker__domain-focus__value" id="focusRegistrable">—</span>
</div>
<div class="o-url-checker__domain-focus__grid">
<div class="o-url-checker__domain-focus__item o-url-checker__surface" id="focusHostBox">
<span class="o-url-checker__domain-focus__label">Domän (med markerade specialtecken)</span>
<span class="o-url-checker__domain-focus__mono" id="focusHost">—</span>
</div>
<div class="o-url-checker__domain-focus__item o-url-checker__surface" id="focusHostNormalizedBox">
<span class="o-url-checker__domain-focus__label">Normaliserad IDN-domän</span>
<span class="o-url-checker__domain-focus__mono o-url-checker__domain-focus__mono--small" id="focusHostNormalized">—</span>
</div>
</div>
<h4 class="u-m-t-1">Länken innehåller</h4>
<div id="signals" class="o-url-checker__pillrow"></div>
<div class="o-url-checker__surface u-m-t-default" id="scriptWarningWrap" hidden>
<span class="o-url-checker__domain-focus__label">Tecken att vara extra uppmärksam på</span>
<p><strong>OBS!</strong> Osynliga tecken, styrtecken och blandade alfabet kan göra att en länk ser trygg ut fast den leder någon annanstans.</p>
<div class="o-url-checker__breakdown__items" id="scriptWarningList"></div>
</div>
</div>
</div>
</div>
<h2 class="u-m-b-0">3. Länkens delar</h2>
<div class="o-url-checker__box o-url-checker__breakdown">
<div class="o-url-checker__box__body" id="breakdownWrap">
<p class="u-m-b-2 o-url-checker__muted">
Tips: Titta extra noga på <strong>domän</strong> och <strong>toppdomän</strong>.<br>
Klicka på en markerad del i URL:en för att visa beskrivningen här.
</p>
<svg class="o-url-checker__breakdown__svg" id="breakdownSvg" aria-hidden="true"></svg>
<div class="o-url-checker__breakdown__url o-url-checker__surface" id="breakdownUrlBox">
<div class="o-url-checker__breakdown__urlcode" id="breakdownUrl"></div>
</div>
<div class="o-url-checker__breakdown__legend o-url-checker__surface">
<span class="o-url-checker__domain-focus__label">Etiketter</span>
<div class="o-url-checker__breakdown__wrap">
<div class="o-url-checker__breakdown__items" id="breakdownLegend"></div>
</div>
<div class="o-url-checker__breakdown__details" id="breakdownDetails"></div>
</div>
</div>
</div>
<div class="u-m-b-default o-url-checker__details" id="detailsSection">
<div class="o-url-checker__box o-url-checker__box--lemon" id="protocolBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Protokoll</h3>
<p class="u-m-b-0 o-url-checker__muted">
Anger hur din webbläsare ska ansluta. <strong>https://</strong> innebär
krypterad anslutning.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outProtocol"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--ruby" id="credentialsBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Inloggningsuppgifter</h3>
<p class="u-m-b-0 o-url-checker__muted">
Användarnamn och lösenord i URL:en (t.ex. <strong>user:password@</strong>).<br>
<strong>Varning:</strong> Detta är osäkert och kan vara ett tecken på phishing.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Användarnamn:</strong> <code id="outUsername"></code></div>
<div><strong>Lösenord:</strong> <code id="outPassword"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="subdomainBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Subdomän</h3>
<p class="u-m-b-0 o-url-checker__muted">
Del före domänen (t.ex. <strong>www</strong>,
<span class="o-url-checker__inlinecode">login</span>). Kan användas vilseledande.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outSubdomain"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="domainBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Domän</h3>
<p class="u-m-b-0 o-url-checker__muted">
Den registrerade "huvudadressen" (t.ex. <strong>microsoft</strong>). Ofta
viktigast för att se <em>vem</em> länken går till.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outDomain"></code></div>
<div><strong>Registrerbar del (förenklad):</strong> <code id="outRegistrable"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="tldBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Toppdomän <span class="u-weight-normal">(Top Level Domain)</span></h3>
<p class="u-m-b-0 o-url-checker__muted">
Sista delen av domänen (t.ex. <strong>.se</strong>, <strong>.com</strong>).
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outTld"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="pathBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Sökväg <span class="u-weight-normal">(/mapp/sida)</span></h3>
<p class="u-m-b-0 o-url-checker__muted">
Delarna efter domänen (t.ex. <strong>/konto/inloggning</strong>).
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outPath"></code></div>
<div><strong>Mappar:</strong> <code id="outFolders"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="queryBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Parametrar</h3>
<p class="u-m-b-0 o-url-checker__muted">
Del efter <strong>?</strong> som skickar extra data (t.ex.
<strong>utm_*</strong>). Kallas även <strong>query-parameter</strong>.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outQuery"></code></div>
<div id="outParamsWrap" hidden>
<strong>Nycklar och värden:</strong>
<ul id="outParams" class="u-m-t-1 u-m-b-0"></ul>
</div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="hashBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Ankare (#)</h3>
<p class="u-m-b-0 o-url-checker__muted">
Del efter <strong>#</strong> som ofta hoppar till en sektion på sidan.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outHash"></code></div>
</div>
</div>
</div>
</section>
</div>
<div class="wrapper u-p-t-4 o-url-checker">
<h1 class="supersize">Förstå en länk (URL)</h1>
<p class="preamble o-url-checker__preamble">
Klistra in en länk här så delas den upp i sina byggstenar. Tanken är att göra det enklare att se
<strong>vem</strong> länken faktiskt går till och <strong>hur</strong> den är uppbyggd – vilket hjälper mot bedrägerilänkar.
</p>
<h2>Så använder du detta mot bedrägerilänkar</h2>
<ul>
<li>
Kontrollera att <strong>domän + toppdomän</strong> stämmer (t.ex. <strong>bank.se</strong>).
</li>
<li>
Var skeptisk om subdomänen innehåller <code>secure</code>, <code>login</code>, <code>verify</code> – det kan vara vilseledande.
</li>
<li>Var försiktig med väldigt långa länkar och många parametrar.</li>
<li>Var uppmärksam på specialtecken som efterliknar "vanliga" tecken.</li>
</ul>
<!-- LEFT: INPUT -->
<h2 class="u-m-b-0">1. Klistra in en länk</h2>
<section class="o-url-checker__box u-m-b-default">
<div class="o-url-checker__box__header">
<p class="u-m-b-0 o-url-checker__muted">
Exempel: <strong>https://microsoft.com/path/to/page?utm_source=x#newsletter</strong>
</p>
</div>
<div class="o-url-checker__box__body">
<div class="field-group u-m-b-0" id="urlInputFieldGroup">
<label for="urlInput" class="u-visuallyhidden">Länk</label>
<textarea
id="urlInput"
class="a-textarea"
rows="5"
placeholder="Klistra in en URL här…"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
></textarea>
<div id="urlInputHelp" class="input-help" aria-live="polite"></div>
</div>
<div class="u-m-t-default o-url-checker__actions">
<button id="analyzeBtn" class="a-button a-button--peacock" type="button">
<span class="a-button__text">Analysera</span>
</button>
<button id="clearBtn" class="a-button a-button--transparent" type="button">
<span class="a-button__text">Rensa</span>
</button>
</div>
<div id="inputHint" class="u-m-t-default o-url-checker__message o-url-checker__message--warn" aria-live="polite"></div>
</div>
</section>
<section id="results" class="u-m-b-default o-url-checker__results" hidden aria-live="polite" aria-atomic="true">
<div id="emptyState" class="o-url-checker__muted">
<p class="u-m-b-0">Klistra in en URL så visas delarna här.</p>
</div>
<div class="u-m-b-4 u-p-t-2" id="overview">
<h2 class="u-m-b-0">2. Snabbkoll</h2>
<div class="o-url-checker__box o-url-checker__domain-focus u-m-b-default">
<div class="o-url-checker__box__body">
<div class="o-url-checker__domain-focus__hint u-m-b-1">
<h3 class="u-m-b-0">Detta är domänen du hamnar på om du klickar på denna länk.</h3>
Kontrollera tecken för tecken att domännamnet är exakt rätt.
</div>
<div class="o-url-checker__domain-focus__main o-url-checker__surface o-url-checker__surface--ash">
<span class="o-url-checker__domain-focus__label">Domän + toppdomän</span>
<span class="o-url-checker__domain-focus__value" id="focusRegistrable">—</span>
</div>
<div class="o-url-checker__domain-focus__grid">
<div class="o-url-checker__domain-focus__item o-url-checker__surface" id="focusHostBox">
<span class="o-url-checker__domain-focus__label">Domän (med markerade specialtecken)</span>
<span class="o-url-checker__domain-focus__mono" id="focusHost">—</span>
</div>
<div class="o-url-checker__domain-focus__item o-url-checker__surface" id="focusHostNormalizedBox">
<span class="o-url-checker__domain-focus__label">Normaliserad IDN-domän</span>
<span class="o-url-checker__domain-focus__mono o-url-checker__domain-focus__mono--small" id="focusHostNormalized">—</span>
</div>
</div>
<h4 class="u-m-t-1">Länken innehåller</h4>
<div id="signals" class="o-url-checker__pillrow"></div>
<div class="o-url-checker__surface u-m-t-default" id="scriptWarningWrap" hidden>
<span class="o-url-checker__domain-focus__label">Tecken att vara extra uppmärksam på</span>
<p><strong>OBS!</strong> Osynliga tecken, styrtecken och blandade alfabet kan göra att en länk ser trygg ut fast den leder någon annanstans.</p>
<div class="o-url-checker__breakdown__items" id="scriptWarningList"></div>
</div>
</div>
</div>
</div>
<h2 class="u-m-b-0">3. Länkens delar</h2>
<div class="o-url-checker__box o-url-checker__breakdown">
<div class="o-url-checker__box__body" id="breakdownWrap">
<p class="u-m-b-2 o-url-checker__muted">
Tips: Titta extra noga på <strong>domän</strong> och <strong>toppdomän</strong>.<br>
Klicka på en markerad del i URL:en för att visa beskrivningen här.
</p>
<svg class="o-url-checker__breakdown__svg" id="breakdownSvg" aria-hidden="true"></svg>
<div class="o-url-checker__breakdown__url o-url-checker__surface" id="breakdownUrlBox">
<div class="o-url-checker__breakdown__urlcode" id="breakdownUrl"></div>
</div>
<div class="o-url-checker__breakdown__legend o-url-checker__surface">
<span class="o-url-checker__domain-focus__label">Etiketter</span>
<div class="o-url-checker__breakdown__wrap">
<div class="o-url-checker__breakdown__items" id="breakdownLegend"></div>
</div>
<div class="o-url-checker__breakdown__details" id="breakdownDetails"></div>
</div>
</div>
</div>
<div class="u-m-b-default o-url-checker__details" id="detailsSection">
<div class="o-url-checker__box o-url-checker__box--lemon" id="protocolBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Protokoll</h3>
<p class="u-m-b-0 o-url-checker__muted">
Anger hur din webbläsare ska ansluta. <strong>https://</strong> innebär
krypterad anslutning.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outProtocol"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--ruby" id="credentialsBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Inloggningsuppgifter</h3>
<p class="u-m-b-0 o-url-checker__muted">
Användarnamn och lösenord i URL:en (t.ex. <strong>user:password@</strong>).<br>
<strong>Varning:</strong> Detta är osäkert och kan vara ett tecken på phishing.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Användarnamn:</strong> <code id="outUsername"></code></div>
<div><strong>Lösenord:</strong> <code id="outPassword"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="subdomainBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Subdomän</h3>
<p class="u-m-b-0 o-url-checker__muted">
Del före domänen (t.ex. <strong>www</strong>,
<span class="o-url-checker__inlinecode">login</span>). Kan användas vilseledande.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outSubdomain"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="domainBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Domän</h3>
<p class="u-m-b-0 o-url-checker__muted">
Den registrerade "huvudadressen" (t.ex. <strong>microsoft</strong>). Ofta
viktigast för att se <em>vem</em> länken går till.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outDomain"></code></div>
<div><strong>Registrerbar del (förenklad):</strong> <code id="outRegistrable"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="tldBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Toppdomän <span class="u-weight-normal">(Top Level Domain)</span></h3>
<p class="u-m-b-0 o-url-checker__muted">
Sista delen av domänen (t.ex. <strong>.se</strong>, <strong>.com</strong>).
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outTld"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="pathBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Sökväg <span class="u-weight-normal">(/mapp/sida)</span></h3>
<p class="u-m-b-0 o-url-checker__muted">
Delarna efter domänen (t.ex. <strong>/konto/inloggning</strong>).
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outPath"></code></div>
<div><strong>Mappar:</strong> <code id="outFolders"></code></div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="queryBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Parametrar</h3>
<p class="u-m-b-0 o-url-checker__muted">
Del efter <strong>?</strong> som skickar extra data (t.ex.
<strong>utm_*</strong>). Kallas även <strong>query-parameter</strong>.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outQuery"></code></div>
<div id="outParamsWrap" hidden>
<strong>Nycklar och värden:</strong>
<ul id="outParams" class="u-m-t-1 u-m-b-0"></ul>
</div>
</div>
</div>
<div class="o-url-checker__box o-url-checker__box--lemon" id="hashBox">
<div class="o-url-checker__box__header">
<h3 class="u-m-b-0">Ankare (#)</h3>
<p class="u-m-b-0 o-url-checker__muted">
Del efter <strong>#</strong> som ofta hoppar till en sektion på sidan.
</p>
</div>
<div class="o-url-checker__box__body o-url-checker__kv">
<div><strong>Värde:</strong> <code id="outHash"></code></div>
</div>
</div>
</div>
</section>
</div>
/* No context defined. */
@charset "UTF-8";
@use '../../configurations/mixins' as mixin;
@use '../../configurations/bem' as bem;
@use '../../configurations/variables' as var;
@use '../../configurations/functions' as func;
@use '../../configurations/colors/colors' as colors;
@include mixin.organism(url-checker) {
@include bem.e(preamble) {
max-width: 60ch;
}
@include bem.e(actions) {
display: flex;
gap: func.rhythm(1);
flex-wrap: wrap;
}
@include bem.e(details) {
display: grid;
gap: func.rhythm(1);
}
@include bem.e(box) {
border: 1px solid colors.$color-concrete;
border-radius: var.$border-radius;
background: colors.$color-snow;
@include bem.m(lemon) {
background: colors.$color-lemon-lighter;
border-color: colors.$color-lemon;
}
@include bem.m(ruby) {
background: colors.$color-ruby-lighter;
border-color: colors.$color-ruby;
}
@include bem.e(header) {
padding: func.rhythm(2) func.rhythm(2) 0 func.rhythm(2);
}
@include bem.e(body) {
padding: func.rhythm(2);
}
}
@include bem.e(kv) {
display: grid;
gap: func.rhythm(0.25);
}
@include bem.e(pillrow) {
display: flex;
flex-wrap: wrap;
gap: func.rhythm(0.5);
margin: func.rhythm(0.5) 0 0;
}
@include bem.e(pill) {
margin: 0;
text-transform: none;
@include bem.m(danger) {
background-color: colors.$color-ruby-lighter;
border-color: colors.$color-ruby;
}
@include bem.m(warn) {
background-color: colors.$color-lemon-lighter;
border-color: colors.$color-lemon-medium-dark;
}
@include bem.m(good) {
background-color: colors.$color-jade-light;
border-color: colors.$color-jade;
}
}
@include bem.e(muted) {
opacity: 0.85;
}
@include bem.e(message) {
border-radius: var.$border-radius;
padding: func.rhythm(0.5);
border-width: 1px;
border-style: solid;
&:empty {
display: none;
}
@include bem.m(danger) {
background-color: colors.$color-ruby-light;
border-color: colors.$color-ruby-dark;
}
@include bem.m(warn) {
background-color: colors.$color-lemon-lighter;
border-color: colors.$color-lemon-medium-dark;
}
@include bem.m(good) {
background-color: colors.$color-jade-light;
border-color: colors.$color-jade;
}
}
@include bem.e(inlinecode) {
font-family: var.$font-family-mono;
font-size: 0.95em;
word-break: break-word;
}
@include bem.e(surface) {
border: 1px solid colors.$color-concrete;
border-radius: var.$border-radius;
background: colors.$color-snow;
padding: func.rhythm(2);
@include bem.m(ash) {
background: colors.$color-ash;
text-shadow: 0 1px 0 colors.$color-snow;
}
}
@include bem.e(script-item) {
&.#{var.$namespace}o-url-checker__breakdown__item {
flex-grow: 0;
cursor: default;
pointer-events: none;
border-color: colors.$color-ruby;
background: colors.$color-ruby-lighter;
}
}
@include bem.e(script-text) {
display: block;
}
@include bem.e(domain-focus) {
@include bem.e(hint) {
margin: 0;
}
@include bem.e(main) {
margin-bottom: func.rhythm(1);
}
@include bem.e(grid) {
display: grid;
gap: func.rhythm(1);
}
@include bem.e(label) {
display: block;
margin-bottom: func.rhythm(0.4);
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.08em;
color: colors.$color-cyberspace;
}
@include bem.e(value) {
display: block;
font-family: var.$font-family-mono;
font-weight: 700;
letter-spacing: 0.1em;
word-break: break-all;
font-size: clamp(1.4rem, 3vw, 2.1rem);
}
@include bem.e(mono) {
display: block;
font-family: var.$font-family-mono;
font-weight: 700;
letter-spacing: 0.1em;
line-height: 1.15;
word-break: break-all;
font-size: clamp(1.1rem, 2.4vw, 1.5rem);
@include bem.m(small) {
font-size: clamp(0.95rem, 2vw, 1.2rem);
}
}
@include bem.e(host-segment) {
&.#{var.$namespace}o-url-checker__breakdown__segment {
cursor: default;
margin-right: 0;
padding: func.rhythm(0.05) func.rhythm(0.12);
outline-width: 1px;
}
@include bem.m(special) {
&.#{var.$namespace}o-url-checker__breakdown__segment {
background: colors.$color-ruby-lighter;
outline-color: colors.$color-ruby;
font-weight: 700;
}
}
}
}
@include bem.e(breakdown) {
position: relative;
overflow: visible;
@include bem.e(wrap) {
display: flex;
flex-direction: column;
flex-wrap: wrap;
position: relative;
}
@include bem.e(url) {
position: relative;
margin-bottom: func.rhythm(1);
}
@include bem.e(urlcode) {
font-family: var.$font-family-mono;
line-height: 1.7;
word-break: break-word;
}
@include bem.e(segment) {
position: relative;
border-radius: var.$border-radius;
padding: func.rhythm(0.08) func.rhythm(0.12);
margin-right: func.rhythm(0.5);
outline: 2px solid transparent;
outline-offset: 1px;
cursor: pointer;
background: rgba(colors.$color-cyberspace, 0.1);
text-shadow: 0 1px 0 colors.$color-snow;
&[data-active='true'],
&:focus,
&:focus-visible {
outline: 2px solid colors.$color-ruby;
outline-offset: 1px;
background-color: colors.$color-lemon-lighter;
box-shadow: none !important;
}
&[data-kind='credentials'] {
background: colors.$color-ruby-lighter;
}
&[data-kind='subdomain'],
&[data-kind='domain'],
&[data-kind='tld'],
&[data-kind='credentials'] {
white-space: nowrap;
}
}
@include bem.e(legend) {
position: relative;
h4 {
margin: 0 0 func.rhythm(0.5);
}
}
@include bem.e(items) {
display: flex;
gap: func.rhythm(1);
justify-items: stretch;
width: 100%;
flex-wrap: wrap;
}
@include bem.e(details) {
margin-top: func.rhythm(1);
.#{var.$namespace}o-url-checker__box {
margin-bottom: 0 !important;
}
}
@include bem.e(item) {
display: flex;
align-items: center;
flex-grow: 1;
padding: func.rhythm(1);
border-radius: var.$border-radius;
border: 1px solid colors.$color-concrete;
background: colors.$color-snow;
cursor: pointer;
> span {
width: 100%;
}
&[data-active='true'] {
box-shadow: 0 0 func.rhythm(1.1) colors.$color-granit;
background-color: colors.$color-lemon-lighter;
border-color: transparent;
outline: 2px solid colors.$color-ruby !important;
}
&[data-active='true'][data-part='credentials'] {
background: colors.$color-ruby-lighter;
}
}
@include bem.e(svg) {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
@include bem.e(line) {
stroke: colors.$color-ruby;
stroke-width: 2;
}
@include bem.e(dot) {
fill: colors.$color-ruby;
}
@include bem.e(hint) {
margin-top: func.rhythm(0.5);
opacity: 0.85;
}
}
code {
font-family: var.$font-family-mono;
background: colors.$color-snow;
border-radius: var.$border-radius;
padding: func.rhythm(0.25);
display: inline-block;
width: auto;
border: 1px solid colors.$color-concrete;
word-break: break-word;
}
}
import { animateAnchorScroll } from '../../assets/js/anchorScroll';
import className from '../../assets/js/className';
const els = {
urlInput: document.getElementById('urlInput'),
urlInputFieldGroup: document.getElementById('urlInputFieldGroup'),
urlInputHelp: document.getElementById('urlInputHelp'),
analyzeBtn: document.getElementById('analyzeBtn'),
clearBtn: document.getElementById('clearBtn'),
inputHint: document.getElementById('inputHint'),
results: document.getElementById('results'),
emptyState: document.getElementById('emptyState'),
signals: document.getElementById('signals'),
focusHost: document.getElementById('focusHost'),
focusHostBox: document.getElementById('focusHostBox'),
focusHostNormalized: document.getElementById('focusHostNormalized'),
focusHostNormalizedBox: document.getElementById('focusHostNormalizedBox'),
focusRegistrable: document.getElementById('focusRegistrable'),
scriptWarningWrap: document.getElementById('scriptWarningWrap'),
scriptWarningList: document.getElementById('scriptWarningList'),
protocolBox: document.getElementById('protocolBox'),
outProtocol: document.getElementById('outProtocol'),
credentialsBox: document.getElementById('credentialsBox'),
outUsername: document.getElementById('outUsername'),
outPassword: document.getElementById('outPassword'),
subdomainBox: document.getElementById('subdomainBox'),
outSubdomain: document.getElementById('outSubdomain'),
domainBox: document.getElementById('domainBox'),
outDomain: document.getElementById('outDomain'),
outRegistrable: document.getElementById('outRegistrable'),
tldBox: document.getElementById('tldBox'),
outTld: document.getElementById('outTld'),
pathBox: document.getElementById('pathBox'),
outPath: document.getElementById('outPath'),
outFolders: document.getElementById('outFolders'),
queryBox: document.getElementById('queryBox'),
outQuery: document.getElementById('outQuery'),
outParamsWrap: document.getElementById('outParamsWrap'),
outParams: document.getElementById('outParams'),
hashBox: document.getElementById('hashBox'),
outHash: document.getElementById('outHash'),
breakdownDetails: document.getElementById('breakdownDetails'),
detailsSection: document.getElementById('detailsSection'),
// NEW: breakdown
breakdownWrap: document.getElementById('breakdownWrap'),
breakdownUrlBox: document.getElementById('breakdownUrlBox'),
breakdownUrl: document.getElementById('breakdownUrl'),
breakdownLegend: document.getElementById('breakdownLegend'),
breakdownSvg: document.getElementById('breakdownSvg'),
};
const shouldInitUrlChecker = Boolean(
document.querySelector(`.${className('o-url-checker')}`) &&
els.urlInput &&
els.analyzeBtn &&
els.clearBtn,
);
const COMMON_2LEVEL_SUFFIXES = new Set([
'co.uk',
'org.uk',
'ac.uk',
'gov.uk',
'com.au',
'net.au',
'org.au',
'co.nz',
'org.nz',
'co.jp',
'ne.jp',
'or.jp',
'com.br',
'com.mx',
'com.tr',
'com.ar',
'com.sg',
'com.my',
'com.hk',
]);
const BREAKDOWN_PARTS = [
{
key: 'protocol',
label: 'Protokoll',
desc: 'http/https',
},
{
key: 'credentials',
label: 'Inloggning',
desc: 'user:pass@',
},
{
key: 'subdomain',
label: 'Subdomän',
desc: 't.ex. www / login',
},
{
key: 'domain',
label: 'Domän',
desc: 'huvudadressen',
},
{
key: 'tld',
label: 'Toppdomän',
desc: '.se / .com',
},
{
key: 'path',
label: 'Sökväg',
desc: '/mapp/sida',
},
{
key: 'query',
label: 'Parametrar',
desc: '?a=b',
},
{
key: 'hash',
label: 'Ankare',
desc: '#sektion',
},
];
const BREAKDOWN_PART_LABELS = Object.fromEntries(
BREAKDOWN_PARTS.map((part) => [part.key, part.label]),
);
const BREAKDOWN_ARIA_ID_PREFIX = `url-checker-breakdown-item-${Math.random().toString(36).slice(2, 10)}`;
const PART_BOX_MAP = {
protocol: 'protocolBox',
credentials: 'credentialsBox',
subdomain: 'subdomainBox',
domain: 'domainBox',
tld: 'tldBox',
path: 'pathBox',
query: 'queryBox',
hash: 'hashBox',
};
const SUSPICIOUS_SCRIPT_PATTERNS = [
{
label: 'Kyrilliska tecken',
ranges: [
[0x0400, 0x04ff],
[0x0500, 0x052f],
[0x1c80, 0x1c8f],
[0x2de0, 0x2dff],
[0xa640, 0xa69f],
],
},
{
label: 'Armeniska tecken',
ranges: [[0x0530, 0x058f]],
},
{
label: 'Grekiska tecken',
ranges: [
[0x0370, 0x03ff],
[0x1f00, 0x1fff],
],
},
{
label: 'Hebreiska tecken',
ranges: [[0x0590, 0x05ff]],
},
{
label: 'Thailändska tecken',
ranges: [[0x0e00, 0x0e7f]],
},
];
const INVISIBLE_CHARACTER_PATTERNS = [
{
label: 'Osynliga tecken',
ranges: [
[0x00ad, 0x00ad],
[0x200b, 0x200d],
[0x2060, 0x2060],
[0xfeff, 0xfeff],
],
displayAsCodePoint: true,
summary: 'Osynliga tecken hittades:',
},
];
const BIDI_CONTROL_PATTERNS = [
{
label: 'Bidi-styrtecken',
ranges: [
[0x061c, 0x061c],
[0x200e, 0x200f],
[0x202a, 0x202e],
[0x2066, 0x2069],
],
displayAsCodePoint: true,
summary: 'Bidi-styrtecken hittades:',
},
];
const FULLWIDTH_CHARACTER_PATTERNS = [
{
label: 'Fullbreddstecken',
ranges: [
[0x3000, 0x3000],
[0x3002, 0x3002],
[0xff01, 0xff0f],
[0xff10, 0xff19],
[0xff1a, 0xff20],
[0xff21, 0xff3a],
[0xff3b, 0xff40],
[0xff41, 0xff5a],
[0xff5b, 0xff60],
[0xffe0, 0xffe6],
],
summary: 'Fullbreddstecken hittades:',
},
];
const LATIN_SCRIPT_RANGES = [
[0x0041, 0x005a],
[0x0061, 0x007a],
[0x00c0, 0x00ff],
[0x0100, 0x017f],
[0x0180, 0x024f],
[0x1e00, 0x1eff],
[0x2c60, 0x2c7f],
[0xa720, 0xa7ff],
[0xab30, 0xab6f],
];
const HOST_SUSPICIOUS_CHARACTER_PATTERNS = [
...INVISIBLE_CHARACTER_PATTERNS,
...BIDI_CONTROL_PATTERNS,
...FULLWIDTH_CHARACTER_PATTERNS,
...SUSPICIOUS_SCRIPT_PATTERNS,
];
const CLASS = {
pill: className('o-url-checker__pill'),
pillGood: className('o-url-checker__pill--good'),
pillWarn: className('o-url-checker__pill--warn'),
pillDanger: className('o-url-checker__pill--danger'),
muted: className('o-url-checker__muted'),
breakdownSegment: className('o-url-checker__breakdown__segment'),
breakdownItem: className('o-url-checker__breakdown__item'),
breakdownLine: className('o-url-checker__breakdown__line'),
breakdownDot: className('o-url-checker__breakdown__dot'),
hostSegment: className('o-url-checker__domain-focus__host-segment'),
hostSegmentSpecial: className(
'o-url-checker__domain-focus__host-segment--special',
),
};
const BREAKDOWN_SEGMENT_SELECTOR = `.${CLASS.breakdownSegment}`;
const BREAKDOWN_ITEM_SELECTOR = `.${CLASS.breakdownItem}`;
const breakdownSegmentPartSelector = (partKey) =>
`${BREAKDOWN_SEGMENT_SELECTOR}[data-part="${partKey}"]`;
const breakdownItemPartSelector = (partKey) =>
`${BREAKDOWN_ITEM_SELECTOR}[data-part="${partKey}"]`;
let visiblePartState = {
availability: {},
};
let detailsMountedInLegend = false;
function safeText(el, value) {
el.textContent = value && String(value).length ? String(value) : '—';
}
function setSafeMarkup(el, markup) {
el.innerHTML = markup;
}
function isCodePointInRanges(codePoint, ranges) {
return ranges.some(([start, end]) => codePoint >= start && codePoint <= end);
}
function formatCodePoint(codePoint) {
return `U+${codePoint.toString(16).toUpperCase().padStart(4, '0')}`;
}
function getPatternByCodePoint(codePoint, patterns) {
for (const pattern of patterns) {
if (isCodePointInRanges(codePoint, pattern.ranges)) return pattern;
}
return null;
}
function formatPatternDetail(char, codePoint, pattern) {
if (pattern?.displayAsCodePoint) return formatCodePoint(codePoint);
return char;
}
function getScriptLabelByCodePoint(codePoint) {
const pattern = getPatternByCodePoint(codePoint, SUSPICIOUS_SCRIPT_PATTERNS);
return pattern ? pattern.label : '';
}
function extractInputHost(rawInput) {
const input = String(rawInput || '').trim();
if (!input) return '';
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(input);
const normalized = hasScheme ? input : `https://${input}`;
const withoutScheme = normalized.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
const authority = withoutScheme.split(/[/?#]/, 1)[0] || '';
const withoutAuth = authority.includes('@')
? authority.split('@').pop() || ''
: authority;
if (!withoutAuth) return '';
if (withoutAuth.startsWith('[')) {
const endBracket = withoutAuth.indexOf(']');
if (endBracket > 0) return withoutAuth.slice(1, endBracket);
}
return withoutAuth.split(':')[0];
}
function buildHostVisualMarkup(hostname) {
const host = String(hostname || '').trim();
if (!host) return '—';
const chunks = [];
let buffer = '';
const flushBuffer = () => {
if (!buffer) return;
chunks.push(escapeHTML(buffer));
buffer = '';
};
for (const char of host) {
const codePoint = char.codePointAt(0);
const pattern =
codePoint === undefined
? null
: getPatternByCodePoint(codePoint, HOST_SUSPICIOUS_CHARACTER_PATTERNS);
if (!pattern) {
buffer += char;
continue;
}
flushBuffer();
const title = escapeHTML(pattern.label);
const visibleChar = escapeHTML(formatPatternDetail(char, codePoint, pattern));
chunks.push(
`<span class="${CLASS.breakdownSegment} ${CLASS.hostSegment} ${CLASS.hostSegmentSpecial}" title="${title}">${visibleChar}</span>`,
);
}
flushBuffer();
return chunks.join('') || '—';
}
function collectPatternFindings(text, patterns) {
const findingsByLabel = new Map(
patterns.map((pattern) => [
pattern.label,
{
...pattern,
count: 0,
details: new Set(),
},
]),
);
for (const char of String(text || '')) {
const codePoint = char.codePointAt(0);
if (codePoint === undefined) continue;
const pattern = getPatternByCodePoint(codePoint, patterns);
if (!pattern) continue;
const finding = findingsByLabel.get(pattern.label);
if (!finding) continue;
finding.count += 1;
finding.details.add(formatPatternDetail(char, codePoint, pattern));
}
return Array.from(findingsByLabel.values())
.filter((finding) => finding.count > 0)
.map((finding) => ({
label: finding.label,
summary: finding.summary || `${finding.count} tecken hittades:`,
details: Array.from(finding.details).join(' '),
}));
}
function detectSuspiciousScripts(text) {
return collectPatternFindings(text, SUSPICIOUS_SCRIPT_PATTERNS);
}
function detectInvisibleCharacters(text) {
return collectPatternFindings(text, INVISIBLE_CHARACTER_PATTERNS);
}
function detectBidiControlCharacters(text) {
return collectPatternFindings(text, BIDI_CONTROL_PATTERNS);
}
function detectFullwidthCharacters(text) {
return collectPatternFindings(text, FULLWIDTH_CHARACTER_PATTERNS);
}
function getHostScriptLabelByCodePoint(codePoint) {
if (isCodePointInRanges(codePoint, LATIN_SCRIPT_RANGES))
return 'Latinska tecken';
return getScriptLabelByCodePoint(codePoint);
}
function detectMixedScriptHostname(hostname) {
const host = String(hostname || '').trim();
if (!host) return null;
const labels = host.split(/[.。。.]/u).filter(Boolean);
const overallScripts = new Set();
const mixedLabels = [];
for (const label of labels) {
const labelScripts = new Set();
for (const char of label) {
if (/[\d-]/.test(char)) continue;
const codePoint = char.codePointAt(0);
if (codePoint === undefined) continue;
const scriptLabel = getHostScriptLabelByCodePoint(codePoint);
if (!scriptLabel) continue;
labelScripts.add(scriptLabel);
overallScripts.add(scriptLabel);
}
if (labelScripts.size > 1) {
mixedLabels.push(
`${label}: ${Array.from(labelScripts).join(' + ')}`,
);
}
}
if (overallScripts.size < 2) return null;
return {
label: 'Blandade alfabet i domänen',
summary: mixedLabels.length
? 'Ett eller flera domänled blandar flera alfabet:'
: 'Domänen innehåller flera alfabet:',
details: mixedLabels.length
? mixedLabels.join(' | ')
: Array.from(overallScripts).join(' + '),
detailsClassName: CLASS.muted,
};
}
function renderScriptWarnings(findings) {
if (!els.scriptWarningWrap || !els.scriptWarningList) return;
els.scriptWarningList.innerHTML = '';
if (!findings.length) {
els.scriptWarningWrap.hidden = true;
return;
}
for (const finding of findings) {
const item = document.createElement('div');
const textWrap = document.createElement('span');
const title = document.createElement('strong');
const desc = document.createElement('span');
const details = document.createElement('span');
item.className = `${CLASS.breakdownItem} ${className('o-url-checker__script-item')}`;
textWrap.className = className('o-url-checker__script-text');
title.textContent = finding.label;
desc.className = CLASS.muted;
desc.textContent = finding.summary || '';
details.className =
finding.detailsClassName || className('o-url-checker__inlinecode');
details.textContent = finding.details || '';
textWrap.appendChild(title);
if (desc.textContent) {
textWrap.appendChild(document.createElement('br'));
textWrap.appendChild(desc);
}
if (details.textContent) {
textWrap.appendChild(document.createElement('br'));
textWrap.appendChild(details);
}
item.appendChild(textWrap);
els.scriptWarningList.appendChild(item);
}
els.scriptWarningWrap.hidden = false;
}
function looksLikeIPAddress(hostname) {
return /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
}
function computeDomainParts(hostname) {
const host = (hostname || '').trim().toLowerCase();
if (!host) return { subdomain: '', domain: '', tld: '', registrable: '' };
if (looksLikeIPAddress(host)) {
return { subdomain: '', domain: host, tld: '(IP)', registrable: host };
}
const labels = host.split('.').filter(Boolean);
if (labels.length === 1) {
return {
subdomain: '',
domain: labels[0],
tld: '',
registrable: labels[0],
};
}
const last2 = labels.slice(-2).join('.');
let suffixLabelsCount = 1;
if (COMMON_2LEVEL_SUFFIXES.has(last2)) suffixLabelsCount = 2;
const tld = labels.slice(-suffixLabelsCount).join('.');
const domainLabelIndex = labels.length - suffixLabelsCount - 1;
const domain = domainLabelIndex >= 0 ? labels[domainLabelIndex] : '';
const subdomain =
domainLabelIndex > 0 ? labels.slice(0, domainLabelIndex).join('.') : '';
const registrable = domain && tld ? `${domain}.${tld}` : host;
return { subdomain, domain, tld, registrable };
}
function parseMaybeURL(raw) {
const input = (raw || '').trim();
if (!input) return { ok: false, reason: 'empty' };
if (/\s/.test(input)) return { ok: false, reason: 'invalid' };
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(input);
const normalized = hasScheme ? input : `https://${input}`;
try {
const url = new URL(normalized);
return {
ok: true,
url,
schemeMissing: !hasScheme,
raw: input,
};
} catch {
return { ok: false, reason: 'invalid' };
}
}
function addSignal(text, kind = 'neutral') {
const pill = document.createElement('span');
const pillText = document.createElement('span');
pill.className = `${className('a-tag')} ${CLASS.pill} u-pointer-events-none u-font-size-medium`;
pillText.className = className('a-tag__text');
if (kind === 'good') pill.classList.add(CLASS.pillGood);
if (kind === 'warn') pill.classList.add(CLASS.pillWarn);
if (kind === 'danger') pill.classList.add(CLASS.pillDanger);
pillText.textContent = text;
pill.appendChild(pillText);
els.signals.appendChild(pill);
}
function setDescribedByToken(el, token, shouldHaveToken) {
if (!el || !token) return;
const tokens = (el.getAttribute('aria-describedby') || '')
.split(/\s+/)
.filter(Boolean);
const nextTokens = shouldHaveToken
? Array.from(new Set([...tokens, token]))
: tokens.filter((existing) => existing !== token);
if (nextTokens.length) el.setAttribute('aria-describedby', nextTokens.join(' '));
else el.removeAttribute('aria-describedby');
}
function setInputErrorAccessibility(hasError) {
if (!els.urlInput) return;
if (hasError) els.urlInput.setAttribute('aria-invalid', 'true');
else els.urlInput.removeAttribute('aria-invalid');
if (els.urlInputFieldGroup) {
els.urlInputFieldGroup.classList.toggle('is-invalid', hasError);
}
setDescribedByToken(els.urlInput, 'results', true);
setDescribedByToken(els.urlInput, 'urlInputHelp', hasError);
}
function setHostSpecialBoxesVisibility(show) {
if (els.focusHostBox) els.focusHostBox.hidden = !show;
if (els.focusHostNormalizedBox) els.focusHostNormalizedBox.hidden = !show;
}
function setVisibleState({ hasResults, errorMessage = '' }) {
const message = (errorMessage || '').trim();
const hasError = Boolean(message);
if (els.urlInputHelp) els.urlInputHelp.textContent = message;
setInputErrorAccessibility(hasError);
els.results.hidden = !hasResults;
els.emptyState.style.display = hasResults ? 'none' : '';
}
// ===== NEW: visual markup rendering =====
function escapeHTML(s) {
return String(s)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
function makeSegment(kind, text) {
if (!text) return '';
const safe = escapeHTML(text);
const partLabel = escapeHTML(BREAKDOWN_PART_LABELS[kind] || kind);
return `<span class="${CLASS.breakdownSegment}" data-kind="${kind}" data-part="${kind}" role="button" tabindex="0" aria-label="Visa del: ${partLabel}">${safe}</span>`;
}
function buildVisualURLParts(
u,
parts,
fallbackHost = '',
includeRootPathSegment = false,
) {
// parts: {subdomain, domain, tld}
const protocol = `${u.protocol}//`;
// Handle credentials (username:password@)
let credentials = '';
if (u.username || u.password) {
const credStr = u.username + (u.password ? `:${u.password}` : '') + '@';
credentials = makeSegment('credentials', credStr);
}
const hostPieces = [];
if (parts.subdomain)
hostPieces.push(makeSegment('subdomain', parts.subdomain + '.'));
if (parts.domain) hostPieces.push(makeSegment('domain', parts.domain));
if (parts.tld) hostPieces.push(makeSegment('tld', '.' + parts.tld));
if (!parts.domain && fallbackHost)
hostPieces.push(makeSegment('domain', fallbackHost)); // fallback
const host = hostPieces.join('');
const hasExplicitPath = Boolean(u.pathname && u.pathname !== '/');
const shouldRenderPath = hasExplicitPath || includeRootPathSegment;
const path = shouldRenderPath ? makeSegment('path', u.pathname || '/') : '';
const query = makeSegment('query', u.search || '');
const hash = makeSegment('hash', u.hash || '');
return (
makeSegment('protocol', protocol) + credentials + host + path + query + hash
);
}
function buildLegend(availableParts) {
els.breakdownLegend.innerHTML = '';
for (const p of BREAKDOWN_PARTS) {
// Only show buttons for parts that exist in the URL
if (!availableParts.has(p.key)) continue;
const item = document.createElement('button');
item.type = 'button';
item.className = CLASS.breakdownItem;
item.id = `${BREAKDOWN_ARIA_ID_PREFIX}-${p.key}`;
item.setAttribute('data-part', p.key);
item.setAttribute('aria-label', `${p.label} – ${p.desc}`);
const txt = document.createElement('span');
txt.innerHTML = `<strong>${p.label}</strong><br/><span class="${CLASS.muted}">${p.desc}</span>`;
item.appendChild(txt);
els.breakdownLegend.appendChild(item);
}
}
function syncBreakdownSegmentAriaDescribedBy() {
els.breakdownUrl.querySelectorAll(BREAKDOWN_SEGMENT_SELECTOR).forEach((segment) => {
const partKey = segment.getAttribute('data-part');
if (!partKey) return;
const legendBtn = els.breakdownLegend.querySelector(breakdownItemPartSelector(partKey));
if (legendBtn && legendBtn.id) segment.setAttribute('aria-describedby', legendBtn.id);
else segment.removeAttribute('aria-describedby');
});
}
function clearActive() {
els.breakdownUrl
.querySelectorAll(BREAKDOWN_SEGMENT_SELECTOR)
.forEach((s) => {
s.dataset.active = 'false';
s.setAttribute('aria-pressed', 'false');
});
els.breakdownLegend
.querySelectorAll(BREAKDOWN_ITEM_SELECTOR)
.forEach((b) => (b.dataset.active = 'false'));
els.breakdownSvg.innerHTML = '';
}
function getBoxCenter(el, relativeTo) {
const r = el.getBoundingClientRect();
const base = relativeTo.getBoundingClientRect();
return {
x: (r.left + r.right) / 2 - base.left,
y: (r.top + r.bottom) / 2 - base.top,
};
}
function drawLine(fromEl, toEl) {
const base = els.breakdownWrap; // svg overlays the entire breakdown wrap
const from = getBoxCenter(fromEl, base);
const to = getBoxCenter(toEl, base);
// SVG sized to the breakdown wrap
const w = base.clientWidth;
const h = base.clientHeight;
els.breakdownSvg.setAttribute('viewBox', `0 0 ${w} ${h}`);
els.breakdownSvg.setAttribute('width', w);
els.breakdownSvg.setAttribute('height', h);
// A polyline with a small dot at each end
const svg = `
<line class="${CLASS.breakdownLine}" x1="${from.x}" y1="${from.y}" x2="${to.x}" y2="${to.y}"></line>
<circle class="${CLASS.breakdownDot}" cx="${from.x}" cy="${from.y}" r="3"></circle>
<circle class="${CLASS.breakdownDot}" cx="${to.x}" cy="${to.y}" r="3"></circle>
`;
els.breakdownSvg.innerHTML = svg;
}
function activatePart(partKey) {
clearActive();
const segment = els.breakdownUrl.querySelector(breakdownSegmentPartSelector(partKey));
const legendBtn = els.breakdownLegend.querySelector(breakdownItemPartSelector(partKey));
if (!segment || !legendBtn) return;
segment.dataset.active = 'true';
segment.setAttribute('aria-pressed', 'true');
legendBtn.dataset.active = 'true';
showSelectedPartBox(partKey);
// Wait one frame so layout changes (e.g. detail box visibility) settle before measuring.
requestAnimationFrame(() => {
const liveSegment = els.breakdownUrl.querySelector(
breakdownSegmentPartSelector(partKey),
);
const liveLegendBtn = els.breakdownLegend.querySelector(
breakdownItemPartSelector(partKey),
);
if (!liveSegment || !liveLegendBtn) return;
drawLine(liveSegment, liveLegendBtn);
});
}
function showSelectedPartBox(partKey) {
for (const boxKey of Object.values(PART_BOX_MAP)) {
if (els[boxKey]) els[boxKey].style.display = 'none';
}
if (!visiblePartState.availability[partKey]) return;
const selectedBox = PART_BOX_MAP[partKey];
if (!selectedBox || !els[selectedBox]) return;
els[selectedBox].style.display = '';
}
function mountDetailBoxesInLegend() {
if (detailsMountedInLegend || !els.breakdownDetails) return;
for (const boxKey of Object.values(PART_BOX_MAP)) {
const box = els[boxKey];
if (box) {
els.breakdownDetails.appendChild(box);
}
}
if (els.detailsSection) {
els.detailsSection.style.display = 'none';
}
detailsMountedInLegend = true;
}
function setupBreakdownInteractions() {
// Click on URL segments
els.breakdownUrl.addEventListener('click', (e) => {
const segment = e.target.closest(BREAKDOWN_SEGMENT_SELECTOR);
if (!segment) return;
activatePart(segment.dataset.part);
});
els.breakdownUrl.addEventListener('keydown', (e) => {
const segment = e.target.closest(BREAKDOWN_SEGMENT_SELECTOR);
if (!segment) return;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
activatePart(segment.dataset.part);
}
});
// Click on legend buttons
els.breakdownLegend.addEventListener('click', (e) => {
const btn = e.target.closest(BREAKDOWN_ITEM_SELECTOR);
if (!btn) return;
activatePart(btn.dataset.part);
});
// Re-draw line on resize if something is active
window.addEventListener('resize', () => {
const activeLegend = els.breakdownLegend.querySelector(
`${BREAKDOWN_ITEM_SELECTOR}[data-active="true"]`,
);
if (!activeLegend) return;
activatePart(activeLegend.dataset.part);
});
}
// ===== existing rendering =====
function render(rawInput) {
els.signals.innerHTML = '';
els.outParams.innerHTML = '';
els.outParamsWrap.hidden = true;
const parsed = parseMaybeURL(rawInput);
mountDetailBoxesInLegend();
if (!rawInput.trim()) {
setVisibleState({ hasResults: false, errorMessage: '' });
els.inputHint.textContent = '';
setHostSpecialBoxesVisibility(false);
renderScriptWarnings([]);
return;
}
if (!parsed.ok) {
setVisibleState({
hasResults: false,
errorMessage:
'Kunde inte tolka länken. Kontrollera att den ser ut som en URL.',
});
els.inputHint.textContent = '';
setHostSpecialBoxesVisibility(false);
renderScriptWarnings([]);
return;
}
const u = parsed.url;
els.inputHint.textContent = parsed.schemeMissing
? 'Tips: Länken saknade protokoll – jag antog https:// för att kunna analysera.'
: '';
const { subdomain, domain, tld, registrable } = computeDomainParts(
u.hostname,
);
const inputHost = extractInputHost(parsed.raw);
const displayHost = inputHost || u.hostname || '';
const {
subdomain: displaySubdomain,
domain: displayDomain,
tld: displayTld,
registrable: displayRegistrable,
} = computeDomainParts(displayHost);
const isIpHost = looksLikeIPAddress(u.hostname);
const hasTopDomain = Boolean(tld && tld !== '(IP)');
if (!hasTopDomain && !isIpHost) {
setVisibleState({
hasResults: false,
errorMessage:
'Domänen saknar toppdomän (t.ex. .se eller .com). Kontrollera att länken är komplett.',
});
els.inputHint.textContent = '';
setHostSpecialBoxesVisibility(false);
renderScriptWarnings([]);
return;
}
const focusHost =
displayDomain && displayTld && displayTld !== '(IP)'
? `${displayDomain}.${displayTld}`
: displayRegistrable || '—';
const invisibleWarnings = detectInvisibleCharacters(parsed.raw);
const bidiWarnings = detectBidiControlCharacters(parsed.raw);
const fullwidthWarnings = detectFullwidthCharacters(parsed.raw);
const suspiciousScripts = detectSuspiciousScripts(parsed.raw);
const mixedScriptHostWarning = detectMixedScriptHostname(displayHost);
const urlWarnings = [
...invisibleWarnings,
...bidiWarnings,
...fullwidthWarnings,
...suspiciousScripts,
...(mixedScriptHostWarning ? [mixedScriptHostWarning] : []),
];
const hostWarnings = [
...detectInvisibleCharacters(displayHost),
...detectBidiControlCharacters(displayHost),
...detectFullwidthCharacters(displayHost),
...detectSuspiciousScripts(displayHost),
...(mixedScriptHostWarning ? [mixedScriptHostWarning] : []),
];
setHostSpecialBoxesVisibility(hostWarnings.length > 0);
if (hostWarnings.length) {
setSafeMarkup(els.focusHost, buildHostVisualMarkup(displayHost));
safeText(els.focusHostNormalized, u.hostname || '—');
}
safeText(els.focusRegistrable, focusHost);
// Determine which parts are available in this URL
const availableParts = new Set(['protocol']); // protocol always present
if (u.username || u.password) availableParts.add('credentials');
if (displaySubdomain) availableParts.add('subdomain');
if (displayDomain) availableParts.add('domain');
if (displayTld && displayTld !== '(IP)') availableParts.add('tld');
if (u.pathname && u.pathname !== '/') availableParts.add('path');
if (u.search) availableParts.add('query');
if (u.hash) availableParts.add('hash');
// NEW: visual URL and legend
buildLegend(availableParts);
els.breakdownUrl.innerHTML = buildVisualURLParts(
u,
{
subdomain: displaySubdomain,
domain: displayDomain,
tld: displayTld,
},
displayHost || u.hostname || '',
Boolean(u.search || u.hash),
);
syncBreakdownSegmentAriaDescribedBy();
clearActive();
// Signals
if (u.protocol === 'https:')
addSignal('HTTPS (krypterad anslutning)', 'good');
else if (u.protocol === 'http:') addSignal('HTTP (inte krypterat)', 'warn');
else addSignal(`Protokoll: ${u.protocol.replace(':', '')}`, 'neutral');
if (looksLikeIPAddress(u.hostname))
addSignal('Värd är en IP-adress', 'warn');
if (u.username || u.password)
addSignal('Inloggningsdel i URL (user:pass@)', 'danger');
if (parsed.raw.includes('@') && !u.username && !u.password)
addSignal('Innehåller @ (kan vara vilseledande)', 'warn');
if (u.hostname.startsWith('xn--') || u.hostname.includes('.xn--'))
addSignal('Punycode (IDN) i domän', 'warn');
if (subdomain && subdomain.split('.').length >= 3)
addSignal('Många subdomäner', 'warn');
renderScriptWarnings(urlWarnings);
if (invisibleWarnings.length)
addSignal('Osynliga tecken i länken', 'danger');
if (bidiWarnings.length)
addSignal('Bidi-styrtecken i länken', 'danger');
if (fullwidthWarnings.length)
addSignal('Fullbreddstecken i länken', 'warn');
if (suspiciousScripts.length)
addSignal('Tecken från andra alfabet i URL:en', 'danger');
if (mixedScriptHostWarning)
addSignal('Blandade alfabet i domänen', 'danger');
const qp = new URLSearchParams(u.search);
const qpCount = Array.from(qp.keys()).length;
if (qpCount === 0) addSignal('Inga parametrar', 'good');
else if (qpCount >= 6)
addSignal(`Många parametrar (${qpCount})`, 'warn');
else addSignal(`Parametrar: ${qpCount}`, 'neutral');
// Outputs
safeText(els.outProtocol, u.protocol ? `${u.protocol}//` : '—');
safeText(els.outUsername, u.username || '—');
safeText(els.outPassword, u.password || '—');
safeText(els.outSubdomain, subdomain || '—');
safeText(els.outDomain, domain || '—');
safeText(els.outRegistrable, registrable || '—');
safeText(els.outTld, tld && tld !== '(IP)' ? tld : '—');
safeText(els.outPath, u.pathname && u.pathname !== '/' ? u.pathname : '—');
const folders = (u.pathname || '/').split('/').filter(Boolean);
safeText(els.outFolders, folders.length ? folders.join(' → ') : '—');
// Query
if (u.search) {
safeText(els.outQuery, u.search);
if (qpCount) {
els.outParamsWrap.hidden = false;
for (const [k, v] of qp.entries()) {
const li = document.createElement('li');
li.appendChild(document.createTextNode(`${k}: `));
const val = document.createElement('code');
val.textContent = v || '—';
li.appendChild(val);
els.outParams.appendChild(li);
}
}
} else {
safeText(els.outQuery, '—');
els.outParamsWrap.hidden = true;
}
safeText(els.outHash, u.hash || '—');
visiblePartState.availability = {
protocol: true,
credentials: Boolean(u.username || u.password),
subdomain: Boolean(subdomain),
domain: Boolean(domain),
tld: Boolean(tld && tld !== '(IP)'),
path: Boolean(u.pathname && u.pathname !== '/'),
query: Boolean(u.search),
hash: Boolean(u.hash),
};
const defaultPart =
[
'protocol',
'domain',
'tld',
'subdomain',
'path',
'query',
'hash',
'credentials',
].find((k) => visiblePartState.availability[k]) || 'protocol';
setTimeout(() => activatePart(defaultPart), 0);
setVisibleState({ hasResults: true, errorMessage: '' });
}
if (shouldInitUrlChecker) {
// Debounced live parsing
let t = null;
let shouldScrollToOverviewOnNextAnalyze = false;
let blockAutoScrollUntilManualAnalyze = false;
const analyze = (value) => {
render(value);
if (!shouldScrollToOverviewOnNextAnalyze) return;
if (blockAutoScrollUntilManualAnalyze) {
shouldScrollToOverviewOnNextAnalyze = false;
return;
}
const errorIsVisible = Boolean(
els.urlInputFieldGroup
&& els.urlInputFieldGroup.classList.contains('is-invalid')
&& els.urlInputHelp
&& els.urlInputHelp.textContent.trim().length,
);
shouldScrollToOverviewOnNextAnalyze = false;
const target = errorIsVisible
? els.urlInputHelp
: document.getElementById('overview');
if (!target) return;
animateAnchorScroll(target, null, {
easing: 'easeOut',
speedAsDuration: false,
});
};
els.urlInput.addEventListener('paste', () => {
shouldScrollToOverviewOnNextAnalyze = true;
});
els.urlInput.addEventListener('input', (event) => {
const inputType = event?.inputType || '';
const isPasteInput = inputType === 'insertFromPaste';
if (!isPasteInput) {
blockAutoScrollUntilManualAnalyze = true;
shouldScrollToOverviewOnNextAnalyze = false;
}
clearTimeout(t);
if (shouldScrollToOverviewOnNextAnalyze) {
analyze(els.urlInput.value);
return;
}
t = setTimeout(() => analyze(els.urlInput.value), 1000);
});
els.analyzeBtn.addEventListener('click', () => {
blockAutoScrollUntilManualAnalyze = false;
shouldScrollToOverviewOnNextAnalyze = Boolean(
els.urlInput.value.trim(),
);
analyze(els.urlInput.value);
});
els.clearBtn.addEventListener('click', () => {
shouldScrollToOverviewOnNextAnalyze = false;
blockAutoScrollUntilManualAnalyze = false;
els.urlInput.value = '';
render('');
els.urlInput.focus();
});
// Init breakdown interactions once
setupBreakdownInteractions();
}
// ===== Accordion functionality =====
function initAccordions() {
document.querySelectorAll('.iis-o-accordion__header').forEach((button) => {
if (button.dataset.accordionInit) return; // Already initialized
button.dataset.accordionInit = 'true';
button.addEventListener('click', function () {
const isExpanded = this.getAttribute('aria-expanded') === 'true';
const panelId = this.getAttribute('aria-controls');
const panel = document.getElementById(panelId);
if (!panel) return;
// Toggle states
this.setAttribute('aria-expanded', !isExpanded);
panel.setAttribute('aria-hidden', isExpanded);
// Toggle visibility
if (isExpanded) {
panel.style.maxHeight = '0';
panel.style.opacity = '0';
panel.style.visibility = 'hidden';
panel.style.overflow = 'hidden';
} else {
panel.style.visibility = 'visible';
panel.style.maxHeight = '1000px';
panel.style.opacity = '1';
panel.style.overflow = 'visible';
}
});
});
}
if (shouldInitUrlChecker) {
// Watch for new accordions being added
const accordionObserver = new MutationObserver(() => {
initAccordions();
});
accordionObserver.observe(document.body, { childList: true, subtree: true });
render('');
}
No notes defined.