text: /**
   * @param {string} text
   */
  function lastLine(text) {
    const splitted = text.split("\n");
    return splitted[splitted.length - 1];
  }
  
  /**
   * @typedef {object} WebIDL2ErrorOptions
   * @property {"error" | "warning"} level
   * @property {Function} autofix
   *
   * @param {string} message error message
   * @param {"Syntax" | "Validation"} kind error type
   * @param {WebIDL2ErrorOptions} [options]
   */
  function error(source, position, current, message, kind, { level = "error", autofix, ruleName } = {}) {
    /**
     * @param {number} count
     */
    function sliceTokens(count) {
      return count > 0 ?
        source.slice(position, position + count) :
        source.slice(Math.max(position + count, 0), position);
    }
  
    function tokensToText(inputs, { precedes } = {}) {
      const text = inputs.map(t => t.trivia + t.value).join("");
      const nextToken = source[position];
      if (nextToken.type === "eof") {
        return text;
      }
      if (precedes) {
        return text + nextToken.trivia;
      }
      return text.slice(nextToken.trivia.length);
    }
  
    const maxTokens = 5; // arbitrary but works well enough
    const line =
      source[position].type !== "eof" ? source[position].line :
      source.length > 1 ? source[position - 1].line :
      1;
  
    const precedingLastLine = lastLine(
      tokensToText(sliceTokens(-maxTokens), { precedes: true })
    );
  
    const subsequentTokens = sliceTokens(maxTokens);
    const subsequentText = tokensToText(subsequentTokens);
    const subsequentFirstLine = subsequentText.split("\n")[0];
  
    const spaced = " ".repeat(precedingLastLine.length) + "^";
    const sourceContext = precedingLastLine + subsequentFirstLine + "\n" + spaced;
  
    const contextType = kind === "Syntax" ? "since" : "inside";
    const inSourceName = source.name ? ` in ${source.name}` : "";
    const grammaticalContext = (current && current.name) ? `, ${contextType} \`${current.partial ? "partial " : ""}${current.type} ${current.name}\`` : "";
    const context = `${kind} error at line ${line}${inSourceName}${grammaticalContext}:\n${sourceContext}`;
    return {
      message: `${context} ${message}`,
      bareMessage: message,
      context,
      line,
      sourceName: source.name,
      level,
      ruleName,
      autofix,
      input: subsequentText,
      tokens: subsequentTokens
    };
  }
  
  /**
   * @param {string} message error message
   */
  export function syntaxError(source, position, current, message) {
    return error(source, position, current, message, "Syntax");
  }
  
  /**
   * @param {string} message error message
   * @param {WebIDL2ErrorOptions} [options]
   */
  export function validationError(token, current, ruleName, message, options = {}) {
    options.ruleName = ruleName;
    return error(current.source, token.index, current, message, "Validation", options);
  }
  