// src/registry.ts
var tokenizers = /* @__PURE__ */ new Map();
var profiles = /* @__PURE__ */ new Map();
var patternCache = /* @__PURE__ */ new Map();
function registerLanguage(code, tokenizer, profile) {
  tokenizers.set(code, tokenizer);
  profiles.set(code, profile);
  patternCache.delete(code);
}

// src/tokenizers/base.ts
var TokenStreamImpl = class {
  constructor(tokens, language) {
    this.pos = 0;
    this.tokens = tokens;
    this.language = language;
  }
  peek(offset = 0) {
    const index = this.pos + offset;
    if (index < 0 || index >= this.tokens.length) {
      return null;
    }
    return this.tokens[index];
  }
  advance() {
    if (this.isAtEnd()) {
      throw new Error("Unexpected end of token stream");
    }
    return this.tokens[this.pos++];
  }
  isAtEnd() {
    return this.pos >= this.tokens.length;
  }
  mark() {
    return { position: this.pos };
  }
  reset(mark) {
    this.pos = mark.position;
  }
  position() {
    return this.pos;
  }
  /**
   * Get remaining tokens as an array.
   */
  remaining() {
    return this.tokens.slice(this.pos);
  }
  /**
   * Consume tokens while predicate is true.
   */
  takeWhile(predicate) {
    const result = [];
    while (!this.isAtEnd() && predicate(this.peek())) {
      result.push(this.advance());
    }
    return result;
  }
  /**
   * Skip tokens while predicate is true.
   */
  skipWhile(predicate) {
    while (!this.isAtEnd() && predicate(this.peek())) {
      this.advance();
    }
  }
};
function createPosition(start, end) {
  return { start, end };
}
function createToken(value, kind, position, normalizedOrOptions) {
  if (typeof normalizedOrOptions === "string") {
    return { value, kind, position, normalized: normalizedOrOptions };
  }
  if (normalizedOrOptions) {
    const { normalized: normalized2, stem, stemConfidence } = normalizedOrOptions;
    const token = { value, kind, position };
    if (normalized2 !== void 0) {
      token.normalized = normalized2;
    }
    if (stem !== void 0) {
      token.stem = stem;
      if (stemConfidence !== void 0) {
        token.stemConfidence = stemConfidence;
      }
    }
    return token;
  }
  return { value, kind, position };
}
function isWhitespace(char) {
  return /\s/.test(char);
}
function isSelectorStart(char) {
  return char === "#" || char === "." || char === "[" || char === "@" || char === "*" || char === "<";
}
function isQuote(char) {
  return char === '"' || char === "'" || char === "`" || char === "\u300C" || char === "\u300D";
}
function isDigit(char) {
  return /\d/.test(char);
}
function isAsciiLetter(char) {
  return /[a-zA-Z]/.test(char);
}
function isAsciiIdentifierChar(char) {
  return /[a-zA-Z0-9_-]/.test(char);
}
function createUnicodeRangeClassifier(ranges) {
  return (char) => {
    const code = char.charCodeAt(0);
    return ranges.some(([start, end]) => code >= start && code <= end);
  };
}
function combineClassifiers(...classifiers) {
  return (char) => classifiers.some((fn) => fn(char));
}
function extractCssSelector(input, startPos) {
  if (startPos >= input.length) return null;
  const char = input[startPos];
  if (!isSelectorStart(char)) return null;
  let pos = startPos;
  let selector = "";
  if (char === "#" || char === ".") {
    selector += input[pos++];
    while (pos < input.length && isAsciiIdentifierChar(input[pos])) {
      selector += input[pos++];
    }
    if (selector.length <= 1) return null;
    if (pos < input.length && input[pos] === "." && char === "#") {
      const methodStart = pos + 1;
      let methodEnd = methodStart;
      while (methodEnd < input.length && isAsciiIdentifierChar(input[methodEnd])) {
        methodEnd++;
      }
      if (methodEnd < input.length && input[methodEnd] === "(") {
        return selector;
      }
    }
  } else if (char === "[") {
    let depth = 1;
    let inQuote = false;
    let quoteChar = null;
    let escaped = false;
    selector += input[pos++];
    while (pos < input.length && depth > 0) {
      const c = input[pos];
      selector += c;
      if (escaped) {
        escaped = false;
      } else if (c === "\\") {
        escaped = true;
      } else if (inQuote) {
        if (c === quoteChar) {
          inQuote = false;
          quoteChar = null;
        }
      } else {
        if (c === '"' || c === "'" || c === "`") {
          inQuote = true;
          quoteChar = c;
        } else if (c === "[") {
          depth++;
        } else if (c === "]") {
          depth--;
        }
      }
      pos++;
    }
    if (depth !== 0) return null;
  } else if (char === "@") {
    selector += input[pos++];
    while (pos < input.length && isAsciiIdentifierChar(input[pos])) {
      selector += input[pos++];
    }
    if (selector.length <= 1) return null;
  } else if (char === "*") {
    selector += input[pos++];
    while (pos < input.length && isAsciiIdentifierChar(input[pos])) {
      selector += input[pos++];
    }
    if (selector.length <= 1) return null;
  } else if (char === "<") {
    selector += input[pos++];
    if (pos >= input.length || !isAsciiLetter(input[pos])) return null;
    while (pos < input.length && isAsciiIdentifierChar(input[pos])) {
      selector += input[pos++];
    }
    while (pos < input.length) {
      const modChar = input[pos];
      if (modChar === ".") {
        selector += input[pos++];
        if (pos >= input.length || !isAsciiIdentifierChar(input[pos])) {
          return null;
        }
        while (pos < input.length && isAsciiIdentifierChar(input[pos])) {
          selector += input[pos++];
        }
      } else if (modChar === "#") {
        selector += input[pos++];
        if (pos >= input.length || !isAsciiIdentifierChar(input[pos])) {
          return null;
        }
        while (pos < input.length && isAsciiIdentifierChar(input[pos])) {
          selector += input[pos++];
        }
      } else if (modChar === "[") {
        let depth = 1;
        let inQuote = false;
        let quoteChar = null;
        let escaped = false;
        selector += input[pos++];
        while (pos < input.length && depth > 0) {
          const c = input[pos];
          selector += c;
          if (escaped) {
            escaped = false;
          } else if (c === "\\") {
            escaped = true;
          } else if (inQuote) {
            if (c === quoteChar) {
              inQuote = false;
              quoteChar = null;
            }
          } else {
            if (c === '"' || c === "'" || c === "`") {
              inQuote = true;
              quoteChar = c;
            } else if (c === "[") {
              depth++;
            } else if (c === "]") {
              depth--;
            }
          }
          pos++;
        }
        if (depth !== 0) return null;
      } else {
        break;
      }
    }
    while (pos < input.length && isWhitespace(input[pos])) {
      selector += input[pos++];
    }
    if (pos < input.length && input[pos] === "/") {
      selector += input[pos++];
      while (pos < input.length && isWhitespace(input[pos])) {
        selector += input[pos++];
      }
    }
    if (pos >= input.length || input[pos] !== ">") return null;
    selector += input[pos++];
  }
  return selector || null;
}
function isPossessiveMarker(input, pos) {
  if (pos >= input.length || input[pos] !== "'") return false;
  if (pos + 1 >= input.length) return false;
  const nextChar = input[pos + 1].toLowerCase();
  if (nextChar !== "s") return false;
  if (pos + 2 >= input.length) return true;
  const afterS = input[pos + 2];
  return isWhitespace(afterS) || afterS === "*" || !isAsciiIdentifierChar(afterS);
}
function extractStringLiteral(input, startPos) {
  if (startPos >= input.length) return null;
  const openQuote = input[startPos];
  if (!isQuote(openQuote)) return null;
  if (openQuote === "'" && isPossessiveMarker(input, startPos)) {
    return null;
  }
  const closeQuoteMap = {
    '"': '"',
    "'": "'",
    "`": "`",
    "\u300C": "\u300D"
  };
  const closeQuote = closeQuoteMap[openQuote];
  if (!closeQuote) return null;
  let pos = startPos + 1;
  let literal = openQuote;
  let escaped = false;
  while (pos < input.length) {
    const char = input[pos];
    literal += char;
    if (escaped) {
      escaped = false;
    } else if (char === "\\") {
      escaped = true;
    } else if (char === closeQuote) {
      return literal;
    }
    pos++;
  }
  return literal;
}
function isUrlStart(input, pos) {
  if (pos >= input.length) return false;
  const char = input[pos];
  const next = input[pos + 1] || "";
  const third = input[pos + 2] || "";
  if (char === "/" && next !== "/" && /[a-zA-Z0-9._-]/.test(next)) {
    return true;
  }
  if (char === "/" && next === "/" && /[a-zA-Z]/.test(third)) {
    return true;
  }
  if (char === "." && (next === "/" || next === "." && third === "/")) {
    return true;
  }
  const slice = input.slice(pos, pos + 8).toLowerCase();
  if (slice.startsWith("http://") || slice.startsWith("https://")) {
    return true;
  }
  return false;
}
function extractUrl(input, startPos) {
  if (!isUrlStart(input, startPos)) return null;
  let pos = startPos;
  let url = "";
  const urlChars = /[a-zA-Z0-9/:._\-?&=%@+~!$'()*,;[\]]/;
  while (pos < input.length) {
    const char = input[pos];
    if (char === "#") {
      if (url.length > 0 && /[a-zA-Z0-9/.]$/.test(url)) {
        url += char;
        pos++;
        while (pos < input.length && /[a-zA-Z0-9_-]/.test(input[pos])) {
          url += input[pos++];
        }
      }
      break;
    }
    if (urlChars.test(char)) {
      url += char;
      pos++;
    } else {
      break;
    }
  }
  if (url.length < 2) return null;
  return url;
}
function extractNumber(input, startPos) {
  if (startPos >= input.length) return null;
  const char = input[startPos];
  if (!isDigit(char) && char !== "-" && char !== "+") return null;
  let pos = startPos;
  let number = "";
  if (input[pos] === "-" || input[pos] === "+") {
    number += input[pos++];
  }
  if (pos >= input.length || !isDigit(input[pos])) {
    return null;
  }
  while (pos < input.length && isDigit(input[pos])) {
    number += input[pos++];
  }
  if (pos < input.length && input[pos] === ".") {
    number += input[pos++];
    while (pos < input.length && isDigit(input[pos])) {
      number += input[pos++];
    }
  }
  if (pos < input.length) {
    const suffix = input.slice(pos, pos + 2);
    if (suffix === "ms") {
      number += "ms";
    } else if (input[pos] === "s" || input[pos] === "m" || input[pos] === "h") {
      number += input[pos];
    }
  }
  return number;
}
var _BaseTokenizer = class _BaseTokenizer {
  constructor() {
    /** Keywords derived from profile, sorted longest-first for greedy matching */
    this.profileKeywords = [];
    /** Map for O(1) keyword lookups by lowercase native word */
    this.profileKeywordMap = /* @__PURE__ */ new Map();
  }
  /**
   * Initialize keyword mappings from a language profile.
   * Builds a list of native→english mappings from:
   * - profile.keywords (primary + alternatives)
   * - profile.references (me, it, you, etc.)
   * - profile.roleMarkers (into, from, with, etc.)
   *
   * Results are sorted longest-first for greedy matching (important for non-space languages).
   * Extras take precedence over profile entries when there are duplicates.
   *
   * @param profile - Language profile containing keyword translations
   * @param extras - Additional keyword entries to include (literals, positional, events)
   */
  initializeKeywordsFromProfile(profile, extras = []) {
    const keywordMap = /* @__PURE__ */ new Map();
    if (profile.keywords) {
      for (const [normalized2, translation] of Object.entries(profile.keywords)) {
        keywordMap.set(translation.primary, {
          native: translation.primary,
          normalized: translation.normalized || normalized2
        });
        if (translation.alternatives) {
          for (const alt of translation.alternatives) {
            keywordMap.set(alt, {
              native: alt,
              normalized: translation.normalized || normalized2
            });
          }
        }
      }
    }
    if (profile.references) {
      for (const [normalized2, native] of Object.entries(profile.references)) {
        keywordMap.set(native, { native, normalized: normalized2 });
      }
    }
    if (profile.roleMarkers) {
      for (const [role, marker] of Object.entries(profile.roleMarkers)) {
        if (marker.primary) {
          keywordMap.set(marker.primary, { native: marker.primary, normalized: role });
        }
        if (marker.alternatives) {
          for (const alt of marker.alternatives) {
            keywordMap.set(alt, { native: alt, normalized: role });
          }
        }
      }
    }
    for (const extra of extras) {
      keywordMap.set(extra.native, extra);
    }
    this.profileKeywords = Array.from(keywordMap.values()).sort(
      (a, b) => b.native.length - a.native.length
    );
    this.profileKeywordMap = /* @__PURE__ */ new Map();
    for (const keyword of this.profileKeywords) {
      this.profileKeywordMap.set(keyword.native.toLowerCase(), keyword);
      const normalized2 = this.removeDiacritics(keyword.native);
      if (normalized2 !== keyword.native && !this.profileKeywordMap.has(normalized2.toLowerCase())) {
        this.profileKeywordMap.set(normalized2.toLowerCase(), keyword);
      }
    }
  }
  /**
   * Remove diacritical marks from a word for normalization.
   * Primarily for Arabic (shadda, fatha, kasra, damma, sukun, etc.)
   * but could be extended for other languages.
   *
   * @param word - Word to normalize
   * @returns Word without diacritics
   */
  removeDiacritics(word) {
    return word.replace(/[\u064B-\u0652\u0670]/g, "");
  }
  /**
   * Try to match a keyword from profile at the current position.
   * Uses longest-first greedy matching (important for non-space languages).
   *
   * @param input - Input string
   * @param pos - Current position
   * @returns Token if matched, null otherwise
   */
  tryProfileKeyword(input, pos) {
    for (const entry of this.profileKeywords) {
      if (input.slice(pos).startsWith(entry.native)) {
        return createToken(
          entry.native,
          "keyword",
          createPosition(pos, pos + entry.native.length),
          entry.normalized
        );
      }
    }
    return null;
  }
  /**
   * Check if the remaining input starts with any known keyword.
   * Useful for non-space languages to detect word boundaries.
   *
   * @param input - Input string
   * @param pos - Current position
   * @returns true if a keyword starts at this position
   */
  isKeywordStart(input, pos) {
    const remaining = input.slice(pos);
    return this.profileKeywords.some((entry) => remaining.startsWith(entry.native));
  }
  /**
   * Look up a keyword by native word (case-insensitive).
   * O(1) lookup using the keyword map.
   *
   * @param native - Native word to look up
   * @returns KeywordEntry if found, undefined otherwise
   */
  lookupKeyword(native) {
    return this.profileKeywordMap.get(native.toLowerCase());
  }
  /**
   * Check if a word is a known keyword (case-insensitive).
   * O(1) lookup using the keyword map.
   *
   * @param native - Native word to check
   * @returns true if the word is a keyword
   */
  isKeyword(native) {
    return this.profileKeywordMap.has(native.toLowerCase());
  }
  /**
   * Set the morphological normalizer for this tokenizer.
   */
  setNormalizer(normalizer) {
    this.normalizer = normalizer;
  }
  /**
   * Try to normalize a word using the morphological normalizer.
   * Returns null if no normalizer is set or normalization fails.
   *
   * Note: We don't check isNormalizable() here because the individual tokenizers
   * historically called normalize() directly without that check. The normalize()
   * method itself handles returning noChange() for words that can't be normalized.
   */
  tryNormalize(word) {
    if (!this.normalizer) return null;
    const result = this.normalizer.normalize(word);
    if (result.stem !== word && result.confidence >= 0.7) {
      return result;
    }
    return null;
  }
  /**
   * Try morphological normalization and keyword lookup.
   *
   * If the word can be normalized to a stem that matches a known keyword,
   * returns a keyword token with morphological metadata (stem, stemConfidence).
   *
   * This is the common pattern for handling conjugated verbs across languages:
   * 1. Normalize the word (e.g., "toggled" → "toggle")
   * 2. Look up the stem in the keyword map
   * 3. Create a token with both the original form and stem metadata
   *
   * @param word - The word to normalize and look up
   * @param startPos - Start position for the token
   * @param endPos - End position for the token
   * @returns Token if stem matches a keyword, null otherwise
   */
  tryMorphKeywordMatch(word, startPos, endPos) {
    const result = this.tryNormalize(word);
    if (!result) return null;
    const stemEntry = this.lookupKeyword(result.stem);
    if (!stemEntry) return null;
    const tokenOptions = {
      normalized: stemEntry.normalized,
      stem: result.stem,
      stemConfidence: result.confidence
    };
    return createToken(word, "keyword", createPosition(startPos, endPos), tokenOptions);
  }
  /**
   * Try to extract a CSS selector at the current position.
   */
  trySelector(input, pos) {
    const selector = extractCssSelector(input, pos);
    if (selector) {
      return createToken(selector, "selector", createPosition(pos, pos + selector.length));
    }
    return null;
  }
  /**
   * Try to extract an event modifier at the current position.
   * Event modifiers are .once, .debounce(N), .throttle(N), .queue(strategy)
   */
  tryEventModifier(input, pos) {
    if (input[pos] !== ".") {
      return null;
    }
    const match = input.slice(pos).match(/^\.(?:once|debounce|throttle|queue)(?:\(([^)]+)\))?(?:\s|$|\.)/);
    if (!match) {
      return null;
    }
    const fullMatch = match[0].replace(/(\s|\.)$/, "");
    const modifierName = fullMatch.slice(1).split("(")[0];
    const value = match[1];
    const token = createToken(
      fullMatch,
      "event-modifier",
      createPosition(pos, pos + fullMatch.length)
    );
    return {
      ...token,
      metadata: {
        modifierName,
        value: value ? modifierName === "queue" ? value : parseInt(value, 10) : void 0
      }
    };
  }
  /**
   * Try to extract a string literal at the current position.
   */
  tryString(input, pos) {
    const literal = extractStringLiteral(input, pos);
    if (literal) {
      return createToken(literal, "literal", createPosition(pos, pos + literal.length));
    }
    return null;
  }
  /**
   * Try to extract a number at the current position.
   */
  tryNumber(input, pos) {
    const number = extractNumber(input, pos);
    if (number) {
      return createToken(number, "literal", createPosition(pos, pos + number.length));
    }
    return null;
  }
  /**
   * Try to match a time unit from a list of patterns.
   *
   * @param input - Input string
   * @param pos - Position after the number
   * @param timeUnits - Array of time unit mappings (native pattern → standard suffix)
   * @param skipWhitespace - Whether to skip whitespace before time unit (default: false)
   * @returns Object with matched suffix and new position, or null if no match
   */
  tryMatchTimeUnit(input, pos, timeUnits, skipWhitespace = false) {
    let unitPos = pos;
    if (skipWhitespace) {
      while (unitPos < input.length && isWhitespace(input[unitPos])) {
        unitPos++;
      }
    }
    const remaining = input.slice(unitPos);
    for (const unit of timeUnits) {
      const candidate = remaining.slice(0, unit.length);
      const matches = unit.caseInsensitive ? candidate.toLowerCase() === unit.pattern.toLowerCase() : candidate === unit.pattern;
      if (matches) {
        if (unit.notFollowedBy) {
          const nextChar = remaining[unit.length] || "";
          if (nextChar === unit.notFollowedBy) continue;
        }
        if (unit.checkBoundary) {
          const nextChar = remaining[unit.length] || "";
          if (isAsciiIdentifierChar(nextChar)) continue;
        }
        return { suffix: unit.suffix, endPos: unitPos + unit.length };
      }
    }
    return null;
  }
  /**
   * Parse a base number (sign, integer, decimal) without time units.
   * Returns the number string and end position.
   *
   * @param input - Input string
   * @param startPos - Start position
   * @param allowSign - Whether to allow +/- sign (default: true)
   * @returns Object with number string and end position, or null
   */
  parseBaseNumber(input, startPos, allowSign = true) {
    let pos = startPos;
    let number = "";
    if (allowSign && (input[pos] === "-" || input[pos] === "+")) {
      number += input[pos++];
    }
    if (pos >= input.length || !isDigit(input[pos])) {
      return null;
    }
    while (pos < input.length && isDigit(input[pos])) {
      number += input[pos++];
    }
    if (pos < input.length && input[pos] === ".") {
      number += input[pos++];
      while (pos < input.length && isDigit(input[pos])) {
        number += input[pos++];
      }
    }
    if (!number || number === "-" || number === "+") return null;
    return { number, endPos: pos };
  }
  /**
   * Try to extract a number with native language time units.
   *
   * This is a template method that handles the common pattern:
   * 1. Parse the base number (sign, integer, decimal)
   * 2. Try to match native language time units
   * 3. Fall back to standard time units (ms, s, m, h)
   *
   * @param input - Input string
   * @param pos - Start position
   * @param nativeTimeUnits - Language-specific time unit mappings
   * @param options - Configuration options
   * @returns Token if number found, null otherwise
   */
  tryNumberWithTimeUnits(input, pos, nativeTimeUnits, options = {}) {
    const { allowSign = true, skipWhitespace = false } = options;
    const baseResult = this.parseBaseNumber(input, pos, allowSign);
    if (!baseResult) return null;
    let { number, endPos } = baseResult;
    const allUnits = [...nativeTimeUnits, ..._BaseTokenizer.STANDARD_TIME_UNITS];
    const timeMatch = this.tryMatchTimeUnit(input, endPos, allUnits, skipWhitespace);
    if (timeMatch) {
      number += timeMatch.suffix;
      endPos = timeMatch.endPos;
    }
    return createToken(number, "literal", createPosition(pos, endPos));
  }
  /**
   * Try to extract a URL at the current position.
   * Handles /path, ./path, ../path, //domain.com, http://, https://
   */
  tryUrl(input, pos) {
    const url = extractUrl(input, pos);
    if (url) {
      return createToken(url, "url", createPosition(pos, pos + url.length));
    }
    return null;
  }
  /**
   * Try to extract a variable reference (:varname) at the current position.
   * In hyperscript, :x refers to a local variable named x.
   */
  tryVariableRef(input, pos) {
    if (input[pos] !== ":") return null;
    if (pos + 1 >= input.length) return null;
    if (!isAsciiIdentifierChar(input[pos + 1])) return null;
    let endPos = pos + 1;
    while (endPos < input.length && isAsciiIdentifierChar(input[endPos])) {
      endPos++;
    }
    const varRef = input.slice(pos, endPos);
    return createToken(varRef, "identifier", createPosition(pos, endPos));
  }
  /**
   * Try to extract an operator or punctuation token at the current position.
   * Handles two-character operators (==, !=, etc.) and single-character operators.
   */
  tryOperator(input, pos) {
    const twoChar = input.slice(pos, pos + 2);
    if (["==", "!=", "<=", ">=", "&&", "||", "->"].includes(twoChar)) {
      return createToken(twoChar, "operator", createPosition(pos, pos + 2));
    }
    const oneChar = input[pos];
    if (["<", ">", "!", "+", "-", "*", "/", "="].includes(oneChar)) {
      return createToken(oneChar, "operator", createPosition(pos, pos + 1));
    }
    if (["(", ")", "{", "}", ",", ";", ":"].includes(oneChar)) {
      return createToken(oneChar, "punctuation", createPosition(pos, pos + 1));
    }
    return null;
  }
  /**
   * Try to match a multi-character particle from a list.
   *
   * Used by languages like Japanese, Korean, and Chinese that have
   * multi-character particles (e.g., Japanese から, まで, より).
   *
   * @param input - Input string
   * @param pos - Current position
   * @param particles - Array of multi-character particles to match
   * @returns Token if matched, null otherwise
   */
  tryMultiCharParticle(input, pos, particles) {
    for (const particle of particles) {
      if (input.slice(pos, pos + particle.length) === particle) {
        return createToken(particle, "particle", createPosition(pos, pos + particle.length));
      }
    }
    return null;
  }
};
/**
 * Configuration for native language time units.
 * Maps patterns to their standard suffix (ms, s, m, h).
 */
_BaseTokenizer.STANDARD_TIME_UNITS = [
  { pattern: "ms", suffix: "ms", length: 2 },
  { pattern: "s", suffix: "s", length: 1, checkBoundary: true },
  { pattern: "m", suffix: "m", length: 1, checkBoundary: true, notFollowedBy: "s" },
  { pattern: "h", suffix: "h", length: 1, checkBoundary: true }
];
var BaseTokenizer = _BaseTokenizer;

// src/tokenizers/morphology/types.ts
function noChange(word) {
  return { stem: word, confidence: 1 };
}
function normalized(stem, confidence, metadata) {
  if (metadata) {
    return { stem, confidence, metadata };
  }
  return { stem, confidence };
}

// src/tokenizers/morphology/korean-normalizer.ts
function isHangul(char) {
  const code = char.charCodeAt(0);
  return code >= 44032 && code <= 55203;
}
function containsKorean(word) {
  for (const char of word) {
    if (isHangul(char)) return true;
  }
  return false;
}
var KOREAN_SUFFIX_RULES = [
  // Honorific conditional/temporal forms (-시- infix)
  // These are critical for polite/formal Korean
  { pattern: "\uD558\uC2DC\uB2C8\uAE4C", confidence: 0.85, conjugationType: "honorific-causal", minStemLength: 1 },
  { pattern: "\uD558\uC2E4\uB54C", confidence: 0.88, conjugationType: "honorific-temporal", minStemLength: 1 },
  { pattern: "\uD558\uC2E4 \uB54C", confidence: 0.88, conjugationType: "honorific-temporal", minStemLength: 1 },
  {
    pattern: "\uD558\uC2DC\uBA74",
    confidence: 0.88,
    conjugationType: "honorific-conditional",
    minStemLength: 1
  },
  {
    pattern: "\uC73C\uC2DC\uBA74",
    confidence: 0.85,
    conjugationType: "honorific-conditional",
    minStemLength: 2
  },
  { pattern: "\uC2DC\uBA74", confidence: 0.82, conjugationType: "honorific-conditional", minStemLength: 2 },
  // Sequential/temporal forms - "after doing", "before doing", "as soon as"
  { pattern: "\uD558\uACE0\uB098\uC11C", confidence: 0.85, conjugationType: "sequential-after", minStemLength: 1 },
  { pattern: "\uD558\uACE0 \uB098\uC11C", confidence: 0.85, conjugationType: "sequential-after", minStemLength: 1 },
  { pattern: "\uD558\uACE0\uC11C", confidence: 0.85, conjugationType: "sequential-after", minStemLength: 1 },
  { pattern: "\uACE0\uB098\uC11C", confidence: 0.82, conjugationType: "sequential-after", minStemLength: 2 },
  { pattern: "\uACE0 \uB098\uC11C", confidence: 0.82, conjugationType: "sequential-after", minStemLength: 2 },
  { pattern: "\uACE0\uC11C", confidence: 0.82, conjugationType: "sequential-after", minStemLength: 2 },
  { pattern: "\uD558\uAE30\uC804\uC5D0", confidence: 0.85, conjugationType: "sequential-before", minStemLength: 1 },
  {
    pattern: "\uD558\uAE30 \uC804\uC5D0",
    confidence: 0.85,
    conjugationType: "sequential-before",
    minStemLength: 1
  },
  { pattern: "\uAE30\uC804\uC5D0", confidence: 0.82, conjugationType: "sequential-before", minStemLength: 2 },
  { pattern: "\uAE30 \uC804\uC5D0", confidence: 0.82, conjugationType: "sequential-before", minStemLength: 2 },
  { pattern: "\uD558\uC790\uB9C8\uC790", confidence: 0.88, conjugationType: "immediate", minStemLength: 1 },
  { pattern: "\uC790\uB9C8\uC790", confidence: 0.85, conjugationType: "immediate", minStemLength: 2 },
  // Obligation forms - "must do", "should do"
  { pattern: "\uD574\uC57C\uD574\uC694", confidence: 0.85, conjugationType: "obligation", minStemLength: 1 },
  { pattern: "\uD574\uC57C\uD574", confidence: 0.85, conjugationType: "obligation", minStemLength: 1 },
  { pattern: "\uD574\uC57C\uD558\uB2E4", confidence: 0.85, conjugationType: "obligation", minStemLength: 1 },
  { pattern: "\uC5B4\uC57C\uD574\uC694", confidence: 0.82, conjugationType: "obligation", minStemLength: 2 },
  { pattern: "\uC5B4\uC57C\uD574", confidence: 0.82, conjugationType: "obligation", minStemLength: 2 },
  { pattern: "\uC544\uC57C\uD574\uC694", confidence: 0.82, conjugationType: "obligation", minStemLength: 2 },
  { pattern: "\uC544\uC57C\uD574", confidence: 0.82, conjugationType: "obligation", minStemLength: 2 },
  // Conditional forms - most natural for event handlers (longest first)
  // These are critical for native Korean idioms like "클릭하면 증가"
  { pattern: "\uD558\uB2C8\uAE4C", confidence: 0.85, conjugationType: "causal-nikka", minStemLength: 1 },
  { pattern: "\uD560\uB54C", confidence: 0.88, conjugationType: "temporal-ttae", minStemLength: 1 },
  { pattern: "\uD560 \uB54C", confidence: 0.88, conjugationType: "temporal-ttae", minStemLength: 1 },
  { pattern: "\uC744\uB54C", confidence: 0.85, conjugationType: "temporal-ttae", minStemLength: 2 },
  { pattern: "\uC744 \uB54C", confidence: 0.85, conjugationType: "temporal-ttae", minStemLength: 2 },
  { pattern: "\uD558\uBA74", confidence: 0.88, conjugationType: "conditional-myeon", minStemLength: 1 },
  { pattern: "\uC73C\uBA74", confidence: 0.85, conjugationType: "conditional-myeon", minStemLength: 2 },
  { pattern: "\uB2C8\uAE4C", confidence: 0.82, conjugationType: "causal-nikka", minStemLength: 2 },
  { pattern: "\uBA74", confidence: 0.8, conjugationType: "conditional-myeon", minStemLength: 2 },
  // Formal polite forms (longest first)
  { pattern: "\uD558\uC600\uC2B5\uB2C8\uB2E4", confidence: 0.85, conjugationType: "past", minStemLength: 1 },
  { pattern: "\uD588\uC2B5\uB2C8\uB2E4", confidence: 0.85, conjugationType: "past", minStemLength: 1 },
  { pattern: "\uD569\uB2C8\uB2E4", confidence: 0.85, conjugationType: "polite", minStemLength: 1 },
  { pattern: "\uC2B5\uB2C8\uB2E4", confidence: 0.82, conjugationType: "polite", minStemLength: 2 },
  { pattern: "\uB429\uB2C8\uB2E4", confidence: 0.82, conjugationType: "polite", minStemLength: 1 },
  { pattern: "\u3142\uB2C8\uB2E4", confidence: 0.82, conjugationType: "polite", minStemLength: 2 },
  // Honorific request forms
  { pattern: "\uD558\uC138\uC694", confidence: 0.85, conjugationType: "honorific", minStemLength: 1 },
  { pattern: "\uD558\uC2ED\uC2DC\uC624", confidence: 0.85, conjugationType: "honorific", minStemLength: 1 },
  { pattern: "\uC138\uC694", confidence: 0.82, conjugationType: "honorific", minStemLength: 2 },
  { pattern: "\uC2ED\uC2DC\uC624", confidence: 0.82, conjugationType: "honorific", minStemLength: 2 },
  // Informal polite (요) forms
  { pattern: "\uD558\uACE0\uC788\uC5B4\uC694", confidence: 0.82, conjugationType: "progressive", minStemLength: 1 },
  { pattern: "\uD558\uACE0\uC788\uC5B4", confidence: 0.82, conjugationType: "progressive", minStemLength: 1 },
  { pattern: "\uD588\uC5B4\uC694", confidence: 0.85, conjugationType: "past", minStemLength: 1 },
  { pattern: "\uD574\uC694", confidence: 0.85, conjugationType: "polite", minStemLength: 1 },
  { pattern: "\uC5B4\uC694", confidence: 0.82, conjugationType: "polite", minStemLength: 2 },
  { pattern: "\uC544\uC694", confidence: 0.82, conjugationType: "polite", minStemLength: 2 },
  // Informal (반말) forms
  { pattern: "\uD588\uC5B4", confidence: 0.85, conjugationType: "past", minStemLength: 1 },
  { pattern: "\uD574", confidence: 0.8, conjugationType: "present", minStemLength: 1 },
  { pattern: "\uC5C8\uC5B4", confidence: 0.82, conjugationType: "past", minStemLength: 2 },
  { pattern: "\uC558\uC5B4", confidence: 0.82, conjugationType: "past", minStemLength: 2 },
  // Progressive forms
  { pattern: "\uD558\uACE0\uC788\uB2E4", confidence: 0.82, conjugationType: "progressive", minStemLength: 1 },
  { pattern: "\uACE0\uC788\uB2E4", confidence: 0.8, conjugationType: "progressive", minStemLength: 2 },
  { pattern: "\uACE0\uC788\uC5B4", confidence: 0.8, conjugationType: "progressive", minStemLength: 2 },
  // Dictionary/infinitive form (하다 verbs)
  { pattern: "\uD558\uB2E4", confidence: 0.88, conjugationType: "dictionary", minStemLength: 1 },
  // Negative forms
  { pattern: "\uD558\uC9C0\uC54A\uB2E4", confidence: 0.82, conjugationType: "negative", minStemLength: 1 },
  { pattern: "\uC548\uD558\uB2E4", confidence: 0.82, conjugationType: "negative", minStemLength: 1 },
  { pattern: "\uC9C0\uC54A\uB2E4", confidence: 0.8, conjugationType: "negative", minStemLength: 2 },
  // Imperative forms
  { pattern: "\uD574\uB77C", confidence: 0.82, conjugationType: "imperative", minStemLength: 1 },
  { pattern: "\uD558\uB77C", confidence: 0.82, conjugationType: "imperative", minStemLength: 1 },
  // Generic verb endings (lower confidence)
  { pattern: "\uB2E4", confidence: 0.75, conjugationType: "dictionary", minStemLength: 2 }
];
var HADA_PATTERNS = [
  // Honorific forms (-시- infix) - polite/formal Korean
  // 클릭하시면 → 클릭 (if you click - honorific)
  { pattern: "\uD558\uC2DC\uB2C8\uAE4C", confidence: 0.88, conjugationType: "honorific-causal" },
  { pattern: "\uD558\uC2E4\uB54C", confidence: 0.88, conjugationType: "honorific-temporal" },
  { pattern: "\uD558\uC2E4 \uB54C", confidence: 0.88, conjugationType: "honorific-temporal" },
  { pattern: "\uD558\uC2DC\uBA74", confidence: 0.88, conjugationType: "honorific-conditional" },
  { pattern: "\uD558\uC168\uC5B4\uC694", confidence: 0.85, conjugationType: "honorific-past" },
  { pattern: "\uD558\uC168\uC5B4", confidence: 0.85, conjugationType: "honorific-past" },
  { pattern: "\uD558\uC2ED\uB2C8\uB2E4", confidence: 0.85, conjugationType: "honorific-polite" },
  // Sequential/temporal forms - "after doing", "before doing", "as soon as"
  { pattern: "\uD558\uACE0\uB098\uC11C", confidence: 0.88, conjugationType: "sequential-after" },
  { pattern: "\uD558\uACE0 \uB098\uC11C", confidence: 0.88, conjugationType: "sequential-after" },
  { pattern: "\uD558\uACE0\uC11C", confidence: 0.88, conjugationType: "sequential-after" },
  { pattern: "\uD558\uAE30\uC804\uC5D0", confidence: 0.88, conjugationType: "sequential-before" },
  { pattern: "\uD558\uAE30 \uC804\uC5D0", confidence: 0.88, conjugationType: "sequential-before" },
  { pattern: "\uD558\uC790\uB9C8\uC790", confidence: 0.88, conjugationType: "immediate" },
  // Obligation forms - "must do", "should do"
  { pattern: "\uD574\uC57C\uD574\uC694", confidence: 0.88, conjugationType: "obligation" },
  { pattern: "\uD574\uC57C\uD574", confidence: 0.88, conjugationType: "obligation" },
  { pattern: "\uD574\uC57C\uD558\uB2E4", confidence: 0.88, conjugationType: "obligation" },
  // Conditional forms - most natural for event handlers (highest priority)
  // 클릭하면 → 클릭 (if clicked)
  { pattern: "\uD558\uB2C8\uAE4C", confidence: 0.88, conjugationType: "causal-nikka" },
  { pattern: "\uD560\uB54C", confidence: 0.88, conjugationType: "temporal-ttae" },
  { pattern: "\uD560 \uB54C", confidence: 0.88, conjugationType: "temporal-ttae" },
  { pattern: "\uD558\uBA74", confidence: 0.88, conjugationType: "conditional-myeon" },
  // Formal
  { pattern: "\uD558\uC600\uC2B5\uB2C8\uB2E4", confidence: 0.85, conjugationType: "past" },
  { pattern: "\uD588\uC2B5\uB2C8\uB2E4", confidence: 0.85, conjugationType: "past" },
  { pattern: "\uD569\uB2C8\uB2E4", confidence: 0.85, conjugationType: "polite" },
  { pattern: "\uD558\uC2ED\uC2DC\uC624", confidence: 0.85, conjugationType: "honorific" },
  { pattern: "\uD558\uC138\uC694", confidence: 0.85, conjugationType: "honorific" },
  // Informal polite
  { pattern: "\uD588\uC5B4\uC694", confidence: 0.85, conjugationType: "past" },
  { pattern: "\uD574\uC694", confidence: 0.85, conjugationType: "polite" },
  // Informal
  { pattern: "\uD588\uC5B4", confidence: 0.85, conjugationType: "past" },
  { pattern: "\uD574", confidence: 0.8, conjugationType: "present" },
  // Progressive
  { pattern: "\uD558\uACE0\uC788\uC5B4\uC694", confidence: 0.82, conjugationType: "progressive" },
  { pattern: "\uD558\uACE0\uC788\uC5B4", confidence: 0.82, conjugationType: "progressive" },
  { pattern: "\uD558\uACE0\uC788\uB2E4", confidence: 0.82, conjugationType: "progressive" },
  // Connective forms (해서 = because/so, 하고 = and)
  { pattern: "\uD574\uC11C", confidence: 0.82, conjugationType: "connective" },
  { pattern: "\uD558\uACE0", confidence: 0.8, conjugationType: "connective" },
  // Negative
  { pattern: "\uD558\uC9C0\uC54A\uC544\uC694", confidence: 0.82, conjugationType: "negative" },
  { pattern: "\uD558\uC9C0\uC54A\uB2E4", confidence: 0.82, conjugationType: "negative" },
  { pattern: "\uC548\uD574\uC694", confidence: 0.82, conjugationType: "negative" },
  { pattern: "\uC548\uD574", confidence: 0.82, conjugationType: "negative" },
  // Imperative
  { pattern: "\uD574\uB77C", confidence: 0.82, conjugationType: "imperative" },
  { pattern: "\uD558\uB77C", confidence: 0.82, conjugationType: "imperative" },
  // Dictionary form
  { pattern: "\uD558\uB2E4", confidence: 0.88, conjugationType: "dictionary" }
];
var KoreanMorphologicalNormalizer = class {
  constructor() {
    this.language = "ko";
  }
  /**
   * Check if a word might be a Korean verb that can be normalized.
   */
  isNormalizable(word) {
    if (!containsKorean(word)) return false;
    if (word.length < 2) return false;
    return true;
  }
  /**
   * Normalize a Korean word to its stem form.
   */
  normalize(word) {
    const compoundResult = this.normalizeCompound(word);
    if (compoundResult) return compoundResult;
    const hadaResult = this.tryHadaNormalization(word);
    if (hadaResult) return hadaResult;
    for (const rule of KOREAN_SUFFIX_RULES) {
      if (word.endsWith(rule.pattern)) {
        const stem = word.slice(0, -rule.pattern.length);
        const minLength = rule.minStemLength ?? 2;
        if (stem.length < minLength) continue;
        const metadata = {
          removedSuffixes: [rule.pattern]
        };
        if (rule.conjugationType) {
          metadata.conjugationType = rule.conjugationType;
        }
        return normalized(stem, rule.confidence, metadata);
      }
    }
    return noChange(word);
  }
  /**
   * Try to normalize a 하다 verb.
   * 하다 verbs are formed by noun + 하다, very common in Korean.
   */
  tryHadaNormalization(word) {
    for (const pattern of HADA_PATTERNS) {
      if (word.endsWith(pattern.pattern)) {
        const stem = word.slice(0, -pattern.pattern.length);
        if (stem.length < 1) continue;
        return normalized(stem, pattern.confidence, {
          removedSuffixes: [pattern.pattern],
          conjugationType: pattern.conjugationType,
          originalForm: "hada-verb"
        });
      }
    }
    return null;
  }
  /**
   * Normalize compound conjugations (multi-layer suffixes).
   * Korean has complex compound forms that combine multiple grammatical elements.
   */
  normalizeCompound(word) {
    const compoundPatterns = [
      // Sequential past forms (after doing, was)
      {
        pattern: "\uD558\uACE0\uB098\uC11C\uC600\uC5B4",
        suffixes: ["\uD558\uACE0\uB098\uC11C", "\uC600\uC5B4"],
        confidence: 0.78,
        conjugationType: "sequential-after",
        minStemLength: 2
      },
      {
        pattern: "\uD558\uACE0\uB098\uC11C\uC600\uB2E4",
        suffixes: ["\uD558\uACE0\uB098\uC11C", "\uC600\uB2E4"],
        confidence: 0.78,
        conjugationType: "sequential-after",
        minStemLength: 2
      },
      {
        pattern: "\uD558\uACE0\uB098\uC11C",
        suffixes: ["\uD558\uACE0", "\uB098\uC11C"],
        confidence: 0.85,
        conjugationType: "sequential-after",
        minStemLength: 2
      },
      // Modal necessity past forms (had to do)
      {
        pattern: "\uD574\uC57C\uD588\uC5B4",
        suffixes: ["\uD574\uC57C", "\uD588\uC5B4"],
        confidence: 0.8,
        conjugationType: "obligation",
        minStemLength: 2
      },
      {
        pattern: "\uD574\uC57C\uD588\uB2E4",
        suffixes: ["\uD574\uC57C", "\uD588\uB2E4"],
        confidence: 0.8,
        conjugationType: "obligation",
        minStemLength: 2
      },
      {
        pattern: "\uD574\uC57C\uD588\uC2B5\uB2C8\uB2E4",
        suffixes: ["\uD574\uC57C", "\uD588\uC2B5\uB2C8\uB2E4"],
        confidence: 0.8,
        conjugationType: "obligation",
        minStemLength: 2
      },
      // Honorific simultaneous forms (while doing, honorific)
      {
        pattern: "\uD558\uC2DC\uBA74\uC11C",
        suffixes: ["\uD558\uC2DC", "\uBA74\uC11C"],
        confidence: 0.82,
        conjugationType: "connective",
        minStemLength: 2
      },
      {
        pattern: "\uD558\uC2DC\uBA70",
        suffixes: ["\uD558\uC2DC", "\uBA70"],
        confidence: 0.82,
        conjugationType: "connective",
        minStemLength: 2
      },
      // Progressive forms with copula
      {
        pattern: "\uD558\uACE0\uC788\uC5C8\uC5B4",
        suffixes: ["\uD558\uACE0", "\uC788\uC5C8\uC5B4"],
        confidence: 0.8,
        conjugationType: "progressive",
        minStemLength: 2
      },
      {
        pattern: "\uD558\uACE0\uC788\uC5C8\uB2E4",
        suffixes: ["\uD558\uACE0", "\uC788\uC5C8\uB2E4"],
        confidence: 0.8,
        conjugationType: "progressive",
        minStemLength: 2
      }
    ];
    for (const {
      pattern,
      suffixes,
      confidence,
      conjugationType,
      minStemLength
    } of compoundPatterns) {
      if (word.endsWith(pattern)) {
        const stem = word.slice(0, -pattern.length);
        if (stem.length < minStemLength) continue;
        return normalized(stem, confidence, {
          removedSuffixes: suffixes,
          conjugationType
        });
      }
    }
    return null;
  }
};
var koreanMorphologicalNormalizer = new KoreanMorphologicalNormalizer();

// src/generators/profiles/korean.ts
var koreanProfile = {
  code: "ko",
  name: "Korean",
  nativeName: "\uD55C\uAD6D\uC5B4",
  direction: "ltr",
  wordOrder: "SOV",
  markingStrategy: "particle",
  usesSpaces: true,
  // Korean uses spaces between words, but particles attach
  verb: {
    position: "end",
    suffixes: ["\uB2E4", "\uC694", "\uB2C8\uB2E4", "\uC138\uC694"],
    subjectDrop: true
  },
  references: {
    me: "\uB098",
    // "I/me" (informal)
    it: "\uADF8\uAC83",
    // "it"
    you: "\uB108",
    // "you" (informal)
    result: "\uACB0\uACFC",
    event: "\uC774\uBCA4\uD2B8",
    target: "\uB300\uC0C1",
    body: "\uBCF8\uBB38"
  },
  possessive: {
    marker: "\uC758",
    // Possessive particle
    markerPosition: "between",
    specialForms: {
      me: "\uB0B4",
      // Contracted form of 나의 (my)
      it: "\uADF8\uAC83\uC758",
      // "its"
      you: "\uB124"
      // Contracted form of 너의 (your)
    },
    keywords: {
      \uB0B4: "me",
      // nae (my)
      \uB124: "you",
      // ne (your)
      \uADF8\uC758: "it"
      // geu-ui (its/his)
    }
  },
  roleMarkers: {
    patient: { primary: "\uC744", alternatives: ["\uB97C"], position: "after" },
    destination: { primary: "\uC5D0", alternatives: ["\uC73C\uB85C", "\uB85C", "\uC5D0\uC11C", "\uC758"], position: "after" },
    source: { primary: "\uC5D0\uC11C", alternatives: ["\uBD80\uD130"], position: "after" },
    style: { primary: "\uB85C", alternatives: ["\uC73C\uB85C"], position: "after" },
    event: { primary: "\uC744", alternatives: ["\uB97C"], position: "after" }
    // Event as object marker
  },
  keywords: {
    // Class/Attribute operations
    toggle: { primary: "\uD1A0\uAE00", alternatives: ["\uC804\uD658"], normalized: "toggle" },
    add: { primary: "\uCD94\uAC00", normalized: "add" },
    remove: { primary: "\uC81C\uAC70", alternatives: ["\uC0AD\uC81C"], normalized: "remove" },
    // Content operations
    put: { primary: "\uB123\uB2E4", alternatives: ["\uB123\uAE30", "\uB193\uAE30"], normalized: "put" },
    append: { primary: "\uCD94\uAC00", normalized: "append" },
    take: { primary: "\uAC00\uC838\uC624\uB2E4", normalized: "take" },
    make: { primary: "\uB9CC\uB4E4\uB2E4", normalized: "make" },
    clone: { primary: "\uBCF5\uC0AC", normalized: "clone" },
    swap: { primary: "\uAD50\uD658", alternatives: ["\uBC14\uAFB8\uB2E4"], normalized: "swap" },
    morph: { primary: "\uBCC0\uD615", alternatives: ["\uBCC0\uD658"], normalized: "morph" },
    // Variable operations
    set: { primary: "\uC124\uC815", normalized: "set" },
    get: { primary: "\uC5BB\uB2E4", alternatives: ["\uAC00\uC838\uC624\uAE30"], normalized: "get" },
    increment: { primary: "\uC99D\uAC00", normalized: "increment" },
    decrement: { primary: "\uAC10\uC18C", normalized: "decrement" },
    log: { primary: "\uB85C\uADF8", normalized: "log" },
    // Visibility
    show: { primary: "\uBCF4\uC774\uB2E4", alternatives: ["\uD45C\uC2DC", "\uBCF4\uC774\uAE30"], normalized: "show" },
    hide: { primary: "\uC228\uAE30\uB2E4", alternatives: ["\uC228\uAE30\uAE30"], normalized: "hide" },
    transition: { primary: "\uC804\uD658", normalized: "transition" },
    // Events
    on: { primary: "\uC5D0", alternatives: ["\uC2DC", "\uB54C", "\uD560 \uB54C"], normalized: "on" },
    trigger: { primary: "\uD2B8\uB9AC\uAC70", normalized: "trigger" },
    send: { primary: "\uBCF4\uB0B4\uB2E4", normalized: "send" },
    // DOM focus
    focus: { primary: "\uD3EC\uCEE4\uC2A4", normalized: "focus" },
    blur: { primary: "\uBE14\uB7EC", normalized: "blur" },
    // Common event names (for event handler patterns)
    click: { primary: "\uD074\uB9AD", normalized: "click" },
    hover: { primary: "\uD638\uBC84", normalized: "hover" },
    submit: { primary: "\uC81C\uCD9C", normalized: "submit" },
    input: { primary: "\uC785\uB825", normalized: "input" },
    change: { primary: "\uBCC0\uACBD", normalized: "change" },
    // Navigation
    go: { primary: "\uC774\uB3D9", normalized: "go" },
    // Async
    wait: { primary: "\uB300\uAE30", normalized: "wait" },
    fetch: { primary: "\uAC00\uC838\uC624\uAE30", normalized: "fetch" },
    settle: { primary: "\uC548\uC815", normalized: "settle" },
    // Control flow
    if: { primary: "\uB9CC\uC57D", normalized: "if" },
    when: { primary: "\uB54C", normalized: "when" },
    where: { primary: "\uC5B4\uB514", normalized: "where" },
    else: { primary: "\uC544\uB2C8\uBA74", normalized: "else" },
    repeat: { primary: "\uBC18\uBCF5", normalized: "repeat" },
    for: { primary: "\uB3D9\uC548", normalized: "for" },
    while: { primary: "\uB3D9\uC548", normalized: "while" },
    continue: { primary: "\uACC4\uC18D", normalized: "continue" },
    halt: { primary: "\uC815\uC9C0", normalized: "halt" },
    throw: { primary: "\uB358\uC9C0\uB2E4", normalized: "throw" },
    call: { primary: "\uD638\uCD9C", normalized: "call" },
    return: { primary: "\uBC18\uD658", normalized: "return" },
    then: { primary: "\uADF8\uB2E4\uC74C", alternatives: ["\uADF8\uB9AC\uACE0", "\uADF8\uB7F0\uD6C4"], normalized: "then" },
    and: { primary: "\uADF8\uB9AC\uACE0", alternatives: ["\uB610\uD55C", "\uBC0F"], normalized: "and" },
    end: { primary: "\uB05D", alternatives: ["\uC885\uB8CC", "\uB9C8\uCE68"], normalized: "end" },
    // Advanced
    js: { primary: "JS\uC2E4\uD589", alternatives: ["js"], normalized: "js" },
    async: { primary: "\uBE44\uB3D9\uAE30", normalized: "async" },
    tell: { primary: "\uB9D0\uD558\uB2E4", normalized: "tell" },
    default: { primary: "\uAE30\uBCF8\uAC12", normalized: "default" },
    init: { primary: "\uCD08\uAE30\uD654", normalized: "init" },
    behavior: { primary: "\uB3D9\uC791", normalized: "behavior" },
    install: { primary: "\uC124\uCE58", normalized: "install" },
    measure: { primary: "\uCE21\uC815", normalized: "measure" },
    // Modifiers
    into: { primary: "\uC73C\uB85C", normalized: "into" },
    before: { primary: "\uC804\uC5D0", normalized: "before" },
    after: { primary: "\uD6C4\uC5D0", normalized: "after" },
    // Event modifiers (for repeat until event)
    until: { primary: "\uAE4C\uC9C0", normalized: "until" },
    event: { primary: "\uC774\uBCA4\uD2B8", normalized: "event" },
    from: { primary: "\uC5D0\uC11C", normalized: "from" }
  },
  tokenization: {
    particles: ["\uC744", "\uB97C", "\uC774", "\uAC00", "\uC740", "\uB294", "\uC5D0", "\uC5D0\uC11C", "\uC73C\uB85C", "\uB85C", "\uC640", "\uACFC", "\uB3C4"],
    boundaryStrategy: "space"
  },
  eventHandler: {
    // Event marker: 할 때 (when/at the time of), used in SOV pattern
    // Pattern: [event] 할 때 [destination] 의 [patient] 를 [action]
    // Example: 클릭 할 때 #button 의 .active 를 토글
    // Compact forms (no space): 클릭할때 .active를토글
    eventMarker: { primary: "\uD560 \uB54C", alternatives: ["\uD560\uB54C", "\uB54C", "\uC5D0"], position: "after" },
    temporalMarkers: ["\uD560 \uB54C", "\uD560\uB54C", "\uB54C"]
    // temporal markers (with and without space)
  }
};

// src/tokenizers/korean.ts
var isHangul2 = createUnicodeRangeClassifier([[44032, 55203]]);
var isJamo = createUnicodeRangeClassifier([
  [4352, 4607],
  // Hangul Jamo
  [12592, 12687]
  // Hangul Compatibility Jamo
]);
var isKorean = combineClassifiers(isHangul2, isJamo);
var PARTICLES = /* @__PURE__ */ new Set([
  // Subject markers
  "\uC774",
  // i - after consonant
  "\uAC00",
  // ga - after vowel
  // Object markers
  "\uC744",
  // eul - after consonant
  "\uB97C",
  // reul - after vowel
  // Topic markers
  "\uC740",
  // eun - after consonant
  "\uB294",
  // neun - after vowel
  // Location/time markers
  "\uC5D0",
  // e - at, to
  "\uC5D0\uC11C",
  // eseo - at (action location), from
  "\uB85C",
  // ro - to, by means (after vowel or ㄹ)
  "\uC73C\uB85C",
  // euro - to, by means (after consonant)
  // Others
  "\uC640",
  // wa - and, with (after vowel)
  "\uACFC",
  // gwa - and, with (after consonant)
  "\uC758",
  // ui - possessive ('s)
  "\uB3C4",
  // do - also
  "\uB9CC",
  // man - only
  "\uBD80\uD130",
  // buteo - from
  "\uAE4C\uC9C0",
  // kkaji - until
  "\uCC98\uB7FC",
  // cheoreom - like
  "\uBCF4\uB2E4"
  // boda - than
]);
var SINGLE_CHAR_PARTICLES = /* @__PURE__ */ new Set([
  "\uC774",
  "\uAC00",
  "\uC744",
  "\uB97C",
  "\uC740",
  "\uB294",
  "\uC5D0",
  "\uB85C",
  "\uC640",
  "\uACFC",
  "\uC758",
  "\uB3C4",
  "\uB9CC"
]);
var MULTI_CHAR_PARTICLES = ["\uC5D0\uC11C", "\uC73C\uB85C", "\uBD80\uD130", "\uAE4C\uC9C0", "\uCC98\uB7FC", "\uBCF4\uB2E4"];
var TEMPORAL_EVENT_SUFFIXES = ["\uD560\uB54C", "\uD558\uBA74", "\uD558\uB2C8\uAE4C", "\uD560 \uB54C"];
var PARTICLE_ROLES = /* @__PURE__ */ new Map([
  // Subject markers (vowel harmony pair)
  [
    "\uC774",
    {
      role: "agent",
      confidence: 0.85,
      variant: "consonant",
      description: "subject marker (after consonant)"
    }
  ],
  [
    "\uAC00",
    {
      role: "agent",
      confidence: 0.85,
      variant: "vowel",
      description: "subject marker (after vowel)"
    }
  ],
  // Object markers (vowel harmony pair)
  [
    "\uC744",
    {
      role: "patient",
      confidence: 0.95,
      variant: "consonant",
      description: "object marker (after consonant)"
    }
  ],
  [
    "\uB97C",
    {
      role: "patient",
      confidence: 0.95,
      variant: "vowel",
      description: "object marker (after vowel)"
    }
  ],
  // Topic markers (vowel harmony pair)
  [
    "\uC740",
    {
      role: "agent",
      confidence: 0.75,
      variant: "consonant",
      description: "topic marker (after consonant)"
    }
  ],
  [
    "\uB294",
    {
      role: "agent",
      confidence: 0.75,
      variant: "vowel",
      description: "topic marker (after vowel)"
    }
  ],
  // Location/time markers
  ["\uC5D0", { role: "destination", confidence: 0.85, description: "at/to marker" }],
  ["\uC5D0\uC11C", { role: "source", confidence: 0.8, description: "at/from marker (action location)" }],
  // Direction/means markers (vowel harmony pair)
  [
    "\uB85C",
    {
      role: "destination",
      confidence: 0.85,
      variant: "vowel",
      description: "to/by means (after vowel or \u3139)"
    }
  ],
  [
    "\uC73C\uB85C",
    {
      role: "destination",
      confidence: 0.85,
      variant: "consonant",
      description: "to/by means (after consonant)"
    }
  ],
  // And/with markers (vowel harmony pair)
  [
    "\uC640",
    { role: "style", confidence: 0.7, variant: "vowel", description: "and/with (after vowel)" }
  ],
  [
    "\uACFC",
    {
      role: "style",
      confidence: 0.7,
      variant: "consonant",
      description: "and/with (after consonant)"
    }
  ],
  // Other markers
  ["\uC758", { role: "patient", confidence: 0.6, description: "possessive marker" }],
  ["\uB3C4", { role: "patient", confidence: 0.65, description: "also/too marker" }],
  ["\uB9CC", { role: "patient", confidence: 0.65, description: "only marker" }],
  ["\uBD80\uD130", { role: "source", confidence: 0.9, description: "from/since marker" }],
  ["\uAE4C\uC9C0", { role: "destination", confidence: 0.75, description: "until/to marker" }],
  ["\uCC98\uB7FC", { role: "manner", confidence: 0.8, description: "like/as marker" }],
  ["\uBCF4\uB2E4", { role: "source", confidence: 0.75, description: "than marker" }]
]);
var KOREAN_EXTRAS = [
  // Values/Literals
  { native: "\uCC38", normalized: "true" },
  { native: "\uAC70\uC9D3", normalized: "false" },
  { native: "\uB110", normalized: "null" },
  { native: "\uBBF8\uC815\uC758", normalized: "undefined" },
  // Positional
  { native: "\uCCAB\uBC88\uC9F8", normalized: "first" },
  { native: "\uB9C8\uC9C0\uB9C9", normalized: "last" },
  { native: "\uB2E4\uC74C", normalized: "next" },
  { native: "\uC774\uC804", normalized: "previous" },
  { native: "\uAC00\uC7A5\uAC00\uAE4C\uC6B4", normalized: "closest" },
  { native: "\uBD80\uBAA8", normalized: "parent" },
  // Events
  { native: "\uD074\uB9AD", normalized: "click" },
  { native: "\uB354\uBE14\uD074\uB9AD", normalized: "dblclick" },
  { native: "\uBCC0\uACBD", normalized: "change" },
  { native: "\uC81C\uCD9C", normalized: "submit" },
  { native: "\uC785\uB825", normalized: "input" },
  { native: "\uB85C\uB4DC", normalized: "load" },
  { native: "\uC2A4\uD06C\uB864", normalized: "scroll" },
  { native: "\uD0A4\uB2E4\uC6B4", normalized: "keydown" },
  { native: "\uD0A4\uC5C5", normalized: "keyup" },
  { native: "\uB9C8\uC6B0\uC2A4\uC624\uBC84", normalized: "mouseover" },
  { native: "\uB9C8\uC6B0\uC2A4\uC544\uC6C3", normalized: "mouseout" },
  // References (additional forms not in profile)
  { native: "\uB0B4", normalized: "my" },
  { native: "\uADF8\uAC83\uC758", normalized: "its" },
  // Logical
  { native: "\uADF8\uB9AC\uACE0", normalized: "and" },
  { native: "\uB610\uB294", normalized: "or" },
  { native: "\uC544\uB2C8", normalized: "not" },
  { native: "\uC774\uB2E4", normalized: "is" },
  // Time units
  { native: "\uCD08", normalized: "s" },
  { native: "\uBC00\uB9AC\uCD08", normalized: "ms" },
  { native: "\uBD84", normalized: "m" },
  { native: "\uC2DC\uAC04", normalized: "h" }
  // Note: Attached particle+verb forms (를토글, 을토글, etc.) are intentionally
  // NOT included because they cause ambiguous parsing. The separate particle + verb
  // pattern (를 + 토글) is preferred for consistent semantic analysis.
  // This follows the same approach as the Japanese tokenizer.
];
var KOREAN_TIME_UNITS = [
  { pattern: "\uBC00\uB9AC\uCD08", suffix: "ms", length: 3 },
  { pattern: "\uC2DC\uAC04", suffix: "h", length: 2 },
  { pattern: "\uCD08", suffix: "s", length: 1 },
  { pattern: "\uBD84", suffix: "m", length: 1 }
];
var KoreanTokenizer = class extends BaseTokenizer {
  constructor() {
    super();
    this.language = "ko";
    this.direction = "ltr";
    this.initializeKeywordsFromProfile(koreanProfile, KOREAN_EXTRAS);
    this.normalizer = new KoreanMorphologicalNormalizer();
  }
  tokenize(input) {
    const tokens = [];
    let pos = 0;
    while (pos < input.length) {
      if (isWhitespace(input[pos])) {
        pos++;
        continue;
      }
      if (isSelectorStart(input[pos])) {
        const modifierToken = this.tryEventModifier(input, pos);
        if (modifierToken) {
          tokens.push(modifierToken);
          pos = modifierToken.position.end;
          continue;
        }
        const selectorToken = this.trySelector(input, pos);
        if (selectorToken) {
          tokens.push(selectorToken);
          pos = selectorToken.position.end;
          continue;
        }
      }
      if (isQuote(input[pos])) {
        const stringToken = this.tryString(input, pos);
        if (stringToken) {
          tokens.push(stringToken);
          pos = stringToken.position.end;
          continue;
        }
      }
      if (isUrlStart(input, pos)) {
        const urlToken = this.tryUrl(input, pos);
        if (urlToken) {
          tokens.push(urlToken);
          pos = urlToken.position.end;
          continue;
        }
      }
      if (isDigit(input[pos])) {
        const numberToken = this.extractKoreanNumber(input, pos);
        if (numberToken) {
          tokens.push(numberToken);
          pos = numberToken.position.end;
          continue;
        }
      }
      const varToken = this.tryVariableRef(input, pos);
      if (varToken) {
        tokens.push(varToken);
        pos = varToken.position.end;
        continue;
      }
      if (isKorean(input[pos])) {
        const wordToken = this.extractKoreanWord(input, pos);
        if (wordToken) {
          const splitResult = this.trySplitTemporalSuffix(wordToken);
          if (splitResult) {
            tokens.push(splitResult.stemToken);
            tokens.push(splitResult.suffixToken);
          } else {
            tokens.push(wordToken);
          }
          pos = wordToken.position.end;
          continue;
        }
      }
      const multiParticle = this.tryMultiCharParticle(input, pos, MULTI_CHAR_PARTICLES);
      if (multiParticle) {
        const metadata = PARTICLE_ROLES.get(multiParticle.value);
        if (metadata) {
          tokens.push({
            ...multiParticle,
            metadata: {
              particleRole: metadata.role,
              particleConfidence: metadata.confidence,
              particleVariant: metadata.variant
            }
          });
        } else {
          tokens.push(multiParticle);
        }
        pos = multiParticle.position.end;
        continue;
      }
      if (SINGLE_CHAR_PARTICLES.has(input[pos])) {
        const particle = input[pos];
        const metadata = PARTICLE_ROLES.get(particle);
        if (metadata) {
          tokens.push({
            ...createToken(particle, "particle", createPosition(pos, pos + 1)),
            metadata: {
              particleRole: metadata.role,
              particleConfidence: metadata.confidence,
              particleVariant: metadata.variant
            }
          });
        } else {
          tokens.push(createToken(particle, "particle", createPosition(pos, pos + 1)));
        }
        pos++;
        continue;
      }
      if (isAsciiIdentifierChar(input[pos])) {
        const asciiToken = this.extractAsciiWord(input, pos);
        if (asciiToken) {
          tokens.push(asciiToken);
          pos = asciiToken.position.end;
          continue;
        }
      }
      pos++;
    }
    return new TokenStreamImpl(tokens, "ko");
  }
  classifyToken(token) {
    if (PARTICLES.has(token)) return "particle";
    if (this.isKeyword(token)) return "keyword";
    if (token.startsWith("#") || token.startsWith(".") || token.startsWith("[")) return "selector";
    if (token.startsWith('"') || token.startsWith("'")) return "literal";
    if (/^\d/.test(token)) return "literal";
    return "identifier";
  }
  /**
   * Extract a Korean word (sequence of Hangul).
   * Prioritizes known keywords, then uses particle-based word boundaries.
   *
   * Uses morphological normalization to handle verb conjugations.
   */
  extractKoreanWord(input, startPos) {
    const maxKeywordLen = 6;
    for (let len = Math.min(maxKeywordLen, input.length - startPos); len >= 2; len--) {
      const candidate = input.slice(startPos, startPos + len);
      let allKorean = true;
      for (let i = 0; i < candidate.length; i++) {
        if (!isKorean(candidate[i])) {
          allKorean = false;
          break;
        }
      }
      if (!allKorean) continue;
      if (PARTICLES.has(candidate) && startPos === startPos) {
        const afterCandidate = startPos + len;
        const nextChar = afterCandidate < input.length ? input[afterCandidate] : "";
        if (nextChar === "" || isWhitespace(nextChar) || !isKorean(nextChar)) {
          return null;
        }
      }
      const keywordEntry2 = this.lookupKeyword(candidate);
      if (keywordEntry2) {
        return createToken(
          candidate,
          "keyword",
          createPosition(startPos, startPos + len),
          keywordEntry2.normalized
        );
      }
      const morphToken2 = this.tryMorphKeywordMatch(candidate, startPos, startPos + len);
      if (morphToken2) return morphToken2;
    }
    let pos = startPos;
    let word = "";
    while (pos < input.length) {
      const char = input[pos];
      const nextChar = pos + 1 < input.length ? input[pos + 1] : "";
      if (word.length === 0 && SINGLE_CHAR_PARTICLES.has(char)) {
        return null;
      }
      if (SINGLE_CHAR_PARTICLES.has(char) && word.length > 0) {
        const isWordBoundary = nextChar === "" || isWhitespace(nextChar) || !isKorean(nextChar) || SINGLE_CHAR_PARTICLES.has(nextChar);
        if (isWordBoundary) {
          break;
        }
      }
      let foundMulti = false;
      for (const particle of MULTI_CHAR_PARTICLES) {
        if (input.slice(pos, pos + particle.length) === particle && word.length > 0) {
          const afterParticle = pos + particle.length;
          const charAfter = afterParticle < input.length ? input[afterParticle] : "";
          if (charAfter === "" || isWhitespace(charAfter) || !isKorean(charAfter)) {
            foundMulti = true;
            break;
          }
        }
      }
      if (foundMulti) break;
      if (isKorean(char)) {
        word += char;
        pos++;
      } else {
        break;
      }
    }
    if (!word) return null;
    if (PARTICLES.has(word)) {
      return null;
    }
    const keywordEntry = this.lookupKeyword(word);
    if (keywordEntry) {
      return createToken(word, "keyword", createPosition(startPos, pos), keywordEntry.normalized);
    }
    const morphToken = this.tryMorphKeywordMatch(word, startPos, pos);
    if (morphToken) return morphToken;
    return createToken(word, "identifier", createPosition(startPos, pos));
  }
  /**
   * Extract an ASCII word (for mixed Korean/English content).
   */
  extractAsciiWord(input, startPos) {
    let pos = startPos;
    let word = "";
    while (pos < input.length && isAsciiIdentifierChar(input[pos])) {
      word += input[pos++];
    }
    if (!word) return null;
    return createToken(word, "identifier", createPosition(startPos, pos));
  }
  /**
   * Extract a number, including Korean time unit suffixes.
   * Korean time units attach directly without whitespace.
   */
  extractKoreanNumber(input, startPos) {
    return this.tryNumberWithTimeUnits(input, startPos, KOREAN_TIME_UNITS, {
      allowSign: false,
      skipWhitespace: false
    });
  }
  /**
   * Try to split a temporal event suffix from a word token.
   * This handles compact forms like 클릭할때 → 클릭 + 할때
   *
   * @returns Split tokens if a suffix is found, null otherwise
   */
  trySplitTemporalSuffix(wordToken) {
    const word = wordToken.value;
    for (const suffix of TEMPORAL_EVENT_SUFFIXES) {
      if (word.endsWith(suffix) && word.length > suffix.length) {
        const stem = word.slice(0, -suffix.length);
        const stemLower = stem.toLowerCase();
        const keywordEntry = this.lookupKeyword(stemLower);
        if (!keywordEntry) continue;
        const stemEnd = wordToken.position.start + stem.length;
        const stemToken = createToken(
          stem,
          "keyword",
          createPosition(wordToken.position.start, stemEnd),
          keywordEntry.normalized
        );
        const suffixToken = createToken(
          suffix,
          "keyword",
          createPosition(stemEnd, wordToken.position.end),
          "when"
          // Normalize temporal suffixes to 'when'
        );
        return { stemToken, suffixToken };
      }
    }
    return null;
  }
};
var koreanTokenizer = new KoreanTokenizer();

// src/languages/ko.ts
registerLanguage("ko", koreanTokenizer, koreanProfile);
export {
  koreanProfile,
  koreanTokenizer
};
//# sourceMappingURL=ko.js.map