All files / src/components OdrlJsonHighlighter.tsx

97.22% Statements 35/36
92.85% Branches 26/28
100% Functions 4/4
97.22% Lines 35/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164            2x                                                         2x                                               2x                                     11x     11x 11x     11x   105x 75x     105x   44x 44x 44x 44x   27x         27x 27x     17x         17x   61x 2x 59x 2x 57x 1x 56x 56x     105x       11x       11x                       11x 11x 11x     11x     207x                      
"use client";
 
import { useMemo } from "react";
 
/* ── ODRL / EDC vocabulary terms that get special accent colouring ── */
 
const ODRL_KEYWORDS = new Set([
  "odrl:permission",
  "odrl:prohibition",
  "odrl:duty",
  "odrl:obligation",
  "odrl:action",
  "odrl:constraint",
  "odrl:leftOperand",
  "odrl:operator",
  "odrl:rightOperand",
  "odrl:assignee",
  "odrl:assigner",
  "odrl:target",
  "odrl:use",
  "odrl:Set",
  "odrl:Offer",
  "odrl:Agreement",
  "odrl:eq",
  "odrl:neq",
  "odrl:gt",
  "odrl:gteq",
  "odrl:lt",
  "odrl:lteq",
  "odrl:isAnyOf",
  "odrl:isAllOf",
  "odrl:isNoneOf",
  "odrl:commercialize",
]);
 
const EDC_KEYWORDS = new Set([
  "edc:PolicyDefinition",
  "edc:policy",
  "edc:purpose",
  "edc:inForceDate",
  "edc:duration",
  "edc:anonymize",
  "edc:pseudonymize",
  "edc:minimizeData",
  "edc:kAnonymity",
  "edc:reIdentify",
  "edc:aggregateOnly",
  "edc:maintainAuditTrail",
  "edc:secureProcessingEnvironment",
  "edc:extractTrainingData",
  "edc:publishIndividualLevel",
  "edc:secondaryUse",
  "edc:thirdPartySharing",
  "edc:logAccess",
  "edc:notifyPatient",
  "edc:patientConsent",
]);
 
/** CSS classes for each token type */
const C = {
  key: "text-purple-400",
  string: "text-emerald-400",
  number: "text-amber-400",
  bool: "text-sky-400",
  null: "text-[var(--text-secondary)] italic",
  brace: "text-[var(--text-secondary)]",
  odrl: "text-cyan-400 font-semibold",
  edc: "text-orange-400 font-semibold",
  punc: "text-[var(--text-secondary)]",
} as const;
 
interface Token {
  cls: string;
  text: string;
}
 
/** Tokenise a JSON string into styled spans. */
function tokenise(json: string): Token[] {
  const tokens: Token[] = [];
  // Match JSON tokens: strings, numbers, booleans, null, structural chars
  const re =
    /("(?:[^"\\]|\\.)*")\s*(:?)|\b(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b|\b(true|false)\b|\b(null)\b|([{}[\],])/g;
  let lastIndex = 0;
  let m: RegExpExecArray | null;
 
  while ((m = re.exec(json)) !== null) {
    // Whitespace / newlines between tokens
    if (m.index > lastIndex) {
      tokens.push({ cls: "", text: json.slice(lastIndex, m.index) });
    }
 
    if (m[1] !== undefined) {
      // String — might be a key (followed by ':') or a value
      const raw = m[1];
      const isKey = m[2] === ":";
      const inner = raw.slice(1, -1); // without quotes
      if (isKey) {
        // Determine if it's an ODRL/EDC keyword
        const cls = ODRL_KEYWORDS.has(inner)
          ? C.odrl
          : EDC_KEYWORDS.has(inner)
            ? C.edc
            : C.key;
        tokens.push({ cls, text: raw });
        tokens.push({ cls: C.punc, text: ":" });
      } else {
        // String value — check for vocabulary terms inside values
        const cls = ODRL_KEYWORDS.has(inner)
          ? C.odrl
          : EDC_KEYWORDS.has(inner)
            ? C.edc
            : C.string;
        tokens.push({ cls, text: raw });
      }
    } else if (m[3] !== undefined) {
      tokens.push({ cls: C.number, text: m[3] });
    } else if (m[4] !== undefined) {
      tokens.push({ cls: C.bool, text: m[4] });
    } else if (m[5] !== undefined) {
      tokens.push({ cls: C.null, text: m[5] });
    E} else if (m[6] !== undefined) {
      tokens.push({ cls: C.brace, text: m[6] });
    }
 
    lastIndex = re.lastIndex;
  }
 
  // Trailing whitespace
  Iif (lastIndex < json.length) {
    tokens.push({ cls: "", text: json.slice(lastIndex) });
  }
 
  return tokens;
}
 
interface OdrlJsonHighlighterProps {
  data: unknown;
  className?: string;
}
 
export default function OdrlJsonHighlighter({
  data,
  className,
}: OdrlJsonHighlighterProps) {
  const tokens = useMemo(() => {
    const json = JSON.stringify(data, null, 2);
    return tokenise(json);
  }, [data]);
 
  return (
    <pre className={`overflow-auto text-xs leading-relaxed ${className ?? ""}`}>
      {tokens.map((t, i) =>
        t.cls ? (
          <span key={i} className={t.cls}>
            {t.text}
          </span>
        ) : (
          t.text
        ),
      )}
    </pre>
  );
}