Url Checker

<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. */
  • Content:
    @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;
    	}
    }
    
  • URL: /components/raw/url-checker/_url-checker.scss
  • Filesystem Path: src/organisms/url-checker/_url-checker.scss
  • Size: 7.1 KB
  • Content:
    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('&', '&amp;')
    		.replaceAll('<', '&lt;')
    		.replaceAll('>', '&gt;')
    		.replaceAll('"', '&quot;')
    		.replaceAll("'", '&#039;');
    }
    
    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('');
    }
    
  • URL: /components/raw/url-checker/url-checker.js
  • Filesystem Path: src/organisms/url-checker/url-checker.js
  • Size: 33.6 KB

No notes defined.