"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _alignTransform = _interopRequireDefault(require("../alignTransform.cjs"));
var _iterateJsdoc = _interopRequireDefault(require("../iterateJsdoc.cjs"));
var _commentParser = require("comment-parser");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const {
  flow: commentFlow
} = _commentParser.transforms;

/**
 * Detects if a line starts with a markdown list marker
 * Supports: -, *, numbered lists (1., 2., etc.)
 * This explicitly excludes hyphens that are part of JSDoc tag syntax
 * @param {string} text - The text to check
 * @param {boolean} isFirstLineOfTag - True if this is the first line (tag line)
 * @returns {boolean} - True if the text starts with a list marker
 */
const startsWithListMarker = (text, isFirstLineOfTag = false) => {
  // On the first line of a tag, the hyphen is typically the JSDoc separator,
  // not a list marker
  if (isFirstLineOfTag) {
    return false;
  }

  // Match lines that start with optional whitespace, then a list marker
  // - or * followed by a space
  // or a number followed by . or ) and a space
  return /^\s*(?:[\-*]|\d+(?:\.|\)))\s+/v.test(text);
};

/**
 * Checks if we should allow extra indentation beyond wrapIndent.
 * This is true for list continuation lines (lines with more indent than wrapIndent
 * that follow a list item).
 * @param {import('comment-parser').Spec} tag - The tag being checked
 * @param {import('../iterateJsdoc.js').Integer} idx - Current line index (0-based in tag.source.slice(1))
 * @returns {boolean} - True if extra indentation should be allowed
 */
const shouldAllowExtraIndent = (tag, idx) => {
  // Check if any previous line in this tag had a list marker
  // idx is 0-based in the continuation lines (tag.source.slice(1))
  // So tag.source[0] is the tag line, tag.source[idx+1] is current line
  let hasSeenListMarker = false;

  // Check all lines from the tag line onwards
  for (let lineIdx = 0; lineIdx <= idx + 1; lineIdx++) {
    const line = tag.source[lineIdx];
    const isFirstLine = lineIdx === 0;
    if (line?.tokens?.description && startsWithListMarker(line.tokens.description, isFirstLine)) {
      hasSeenListMarker = true;
      break;
    }
  }
  return hasSeenListMarker;
};

/**
 * @typedef {{
 *   postDelimiter: import('../iterateJsdoc.js').Integer,
 *   postHyphen: import('../iterateJsdoc.js').Integer,
 *   postName: import('../iterateJsdoc.js').Integer,
 *   postTag: import('../iterateJsdoc.js').Integer,
 *   postType: import('../iterateJsdoc.js').Integer,
 * }} CustomSpacings
 */

/**
 * @param {import('../iterateJsdoc.js').Utils} utils
 * @param {import('comment-parser').Spec & {
 *   line: import('../iterateJsdoc.js').Integer
 * }} tag
 * @param {CustomSpacings} customSpacings
 */
const checkNotAlignedPerTag = (utils, tag, customSpacings) => {
  /*
  start +
  delimiter +
  postDelimiter +
  tag +
  postTag +
  type +
  postType +
  name +
  postName +
  description +
  end +
  lineEnd
   */

  /**
   * @typedef {"tag"|"type"|"name"|"description"} ContentProp
   */

  /** @type {("postDelimiter"|"postTag"|"postType"|"postName")[]} */
  let spacerProps;
  /** @type {ContentProp[]} */
  let contentProps;
  const mightHaveNamepath = utils.tagMightHaveNameOrNamepath(tag.tag);
  if (mightHaveNamepath) {
    spacerProps = ['postDelimiter', 'postTag', 'postType', 'postName'];
    contentProps = ['tag', 'type', 'name', 'description'];
  } else {
    spacerProps = ['postDelimiter', 'postTag', 'postType'];
    contentProps = ['tag', 'type', 'description'];
  }
  const {
    tokens
  } = tag.source[0];

  /**
   * @param {import('../iterateJsdoc.js').Integer} idx
   * @param {(notRet: boolean, contentProp: ContentProp) => void} [callbck]
   */
  const followedBySpace = (idx, callbck) => {
    const nextIndex = idx + 1;
    return spacerProps.slice(nextIndex).some((spacerProp, innerIdx) => {
      const contentProp = contentProps[nextIndex + innerIdx];
      const spacePropVal = tokens[spacerProp];
      const ret = spacePropVal;
      if (callbck) {
        callbck(!ret, contentProp);
      }
      return ret && (callbck || !contentProp);
    });
  };
  const postHyphenSpacing = customSpacings?.postHyphen ?? 1;
  const exactHyphenSpacing = new RegExp(`^\\s*-\\s{${postHyphenSpacing},${postHyphenSpacing}}(?!\\s)`, 'v');
  const hasNoHyphen = !/^\s*-(?!$)(?=\s)/v.test(tokens.description);
  const hasExactHyphenSpacing = exactHyphenSpacing.test(tokens.description);

  // If checking alignment on multiple lines, need to check other `source`
  //   items
  // Go through `post*` spacing properties and exit to indicate problem if
  //   extra spacing detected
  const ok = !spacerProps.some((spacerProp, idx) => {
    const contentProp = contentProps[idx];
    const contentPropVal = tokens[contentProp];
    const spacerPropVal = tokens[spacerProp];
    const spacing = customSpacings?.[spacerProp] || 1;

    // There will be extra alignment if...

    // 1. The spaces don't match the space it should have (1 or custom spacing) OR
    return spacerPropVal.length !== spacing && spacerPropVal.length !== 0 ||
    // 2. There is a (single) space, no immediate content, and yet another
    //     space is found subsequently (not separated by intervening content)
    spacerPropVal && !contentPropVal && followedBySpace(idx);
  }) && (hasNoHyphen || hasExactHyphenSpacing);
  if (ok) {
    return;
  }
  const fix = () => {
    for (const [idx, spacerProp] of spacerProps.entries()) {
      const contentProp = contentProps[idx];
      const contentPropVal = tokens[contentProp];
      if (contentPropVal) {
        const spacing = customSpacings?.[spacerProp] || 1;
        tokens[spacerProp] = ''.padStart(spacing, ' ');
        followedBySpace(idx, (hasSpace, contentPrp) => {
          if (hasSpace) {
            tokens[contentPrp] = '';
          }
        });
      } else {
        tokens[spacerProp] = '';
      }
    }
    if (!hasExactHyphenSpacing) {
      const hyphenSpacing = /^\s*-\s+/v;
      tokens.description = tokens.description.replace(hyphenSpacing, '-' + ''.padStart(postHyphenSpacing, ' '));
    }
    utils.setTag(tag, tokens);
  };
  utils.reportJSDoc('Expected JSDoc block lines to not be aligned.', tag, fix, true);
};

/**
 * @param {object} cfg
 * @param {CustomSpacings} cfg.customSpacings
 * @param {string} cfg.indent
 * @param {import('comment-parser').Block} cfg.jsdoc
 * @param {import('eslint').Rule.Node & {
 *   range: [number, number]
 * }} cfg.jsdocNode
 * @param {boolean} cfg.preserveMainDescriptionPostDelimiter
 * @param {import('../iterateJsdoc.js').Report} cfg.report
 * @param {string[]} cfg.tags
 * @param {import('../iterateJsdoc.js').Utils} cfg.utils
 * @param {string} cfg.wrapIndent
 * @param {boolean} cfg.disableWrapIndent
 * @returns {void}
 */
const checkAlignment = ({
  customSpacings,
  disableWrapIndent,
  indent,
  jsdoc,
  jsdocNode,
  preserveMainDescriptionPostDelimiter,
  report,
  tags,
  utils,
  wrapIndent
}) => {
  const transform = commentFlow((0, _alignTransform.default)({
    customSpacings,
    disableWrapIndent,
    indent,
    preserveMainDescriptionPostDelimiter,
    tags,
    wrapIndent
  }));
  const transformedJsdoc = transform(jsdoc);
  const comment = '/*' +
  /**
   * @type {import('eslint').Rule.Node & {
   *   range: [number, number], value: string
   * }}
   */
  jsdocNode.value + '*/';
  const formatted = utils.stringify(transformedJsdoc).trimStart();
  if (comment !== formatted) {
    report('Expected JSDoc block lines to be aligned.', /** @type {import('eslint').Rule.ReportFixer} */fixer => {
      return fixer.replaceText(jsdocNode, formatted);
    });
  }
};
var _default = exports.default = (0, _iterateJsdoc.default)(({
  context,
  indent,
  jsdoc,
  jsdocNode,
  report,
  utils
}) => {
  const {
    customSpacings,
    disableWrapIndent = false,
    preserveMainDescriptionPostDelimiter,
    tags: applicableTags = ['param', 'arg', 'argument', 'property', 'prop', 'returns', 'return', 'template'],
    wrapIndent = ''
  } = context.options[1] || {};
  if (context.options[0] === 'always') {
    // Skip if it contains only a single line.
    if (!(
    /**
     * @type {import('eslint').Rule.Node & {
     *   range: [number, number], value: string
     * }}
     */
    jsdocNode.value.includes('\n'))) {
      return;
    }
    checkAlignment({
      customSpacings,
      disableWrapIndent,
      indent,
      jsdoc,
      jsdocNode,
      preserveMainDescriptionPostDelimiter,
      report,
      tags: applicableTags,
      utils,
      wrapIndent
    });
    return;
  }
  const foundTags = utils.getPresentTags(applicableTags);
  if (context.options[0] !== 'any') {
    for (const tag of foundTags) {
      checkNotAlignedPerTag(utils,
      /**
       * @type {import('comment-parser').Spec & {
       *   line: import('../iterateJsdoc.js').Integer
       * }}
       */
      tag, customSpacings);
    }
  }
  for (const tag of foundTags) {
    if (tag.source.length > 1) {
      let idx = 0;
      for (const {
        tokens
        // Avoid the tag line
      } of tag.source.slice(1)) {
        idx++;
        if (!tokens.description ||
        // Avoid first lines after multiline type
        tokens.type || tokens.name) {
          continue;
        }

        // Don't include a single separating space/tab
        const actualIndent = tokens.postDelimiter.slice(1);
        const hasCorrectWrapIndent = actualIndent === wrapIndent;

        // Allow extra indentation if this line or previous lines contain list markers
        // This preserves nested list structure
        const hasExtraIndent = actualIndent.length > wrapIndent.length && actualIndent.startsWith(wrapIndent);
        const isInListContext = shouldAllowExtraIndent(tag, idx - 1);
        if (!disableWrapIndent && !hasCorrectWrapIndent && !(hasExtraIndent && isInListContext)) {
          utils.reportJSDoc('Expected wrap indent', {
            line: tag.source[0].number + idx
          }, () => {
            tokens.postDelimiter = tokens.postDelimiter.charAt(0) + wrapIndent;
          });
          return;
        }
      }
    }
  }
}, {
  iterateAllJsdocs: true,
  meta: {
    docs: {
      description: 'Reports invalid alignment of JSDoc block lines.',
      url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-line-alignment.md#repos-sticky-header'
    },
    fixable: 'whitespace',
    schema: [{
      description: `If the string value is
\`"always"\` then a problem is raised when the lines are not aligned.
If it is \`"never"\` then a problem should be raised when there is more than
one space between each line's parts. If it is \`"any"\`, no alignment is made.
Defaults to \`"never"\`.

Note that in addition to alignment, the "never" and "always" options will both
ensure that at least one space is present after the asterisk delimiter.`,
      enum: ['always', 'never', 'any'],
      type: 'string'
    }, {
      additionalProperties: false,
      properties: {
        customSpacings: {
          additionalProperties: false,
          description: `An object with any of the following spacing keys set to an integer.
If a spacing is not defined, it defaults to one.`,
          properties: {
            postDelimiter: {
              description: 'Affects spacing after the asterisk (e.g., `*   @param`)',
              type: 'integer'
            },
            postHyphen: {
              description: 'Affects spacing after any hyphens in the description (e.g., `* @param {someType} name -  A description`)',
              type: 'integer'
            },
            postName: {
              description: 'Affects spacing after the name (e.g., `* @param {someType} name   `)',
              type: 'integer'
            },
            postTag: {
              description: 'Affects spacing after the tag (e.g., `* @param  `)',
              type: 'integer'
            },
            postType: {
              description: 'Affects spacing after the type (e.g., `* @param {someType}   `)',
              type: 'integer'
            }
          },
          type: 'object'
        },
        disableWrapIndent: {
          description: 'Disables `wrapIndent`; existing wrap indentation is preserved without changes.',
          type: 'boolean'
        },
        preserveMainDescriptionPostDelimiter: {
          default: false,
          description: `A boolean to determine whether to preserve the post-delimiter spacing of the
main description. If \`false\` or unset, will be set to a single space.`,
          type: 'boolean'
        },
        tags: {
          description: `Use this to change the tags which are sought for alignment changes. Defaults to an array of
\`['param', 'arg', 'argument', 'property', 'prop', 'returns', 'return', 'template']\`.`,
          items: {
            type: 'string'
          },
          type: 'array'
        },
        wrapIndent: {
          description: `The indent that will be applied for tag text after the first line.
Default to the empty string (no indent).`,
          type: 'string'
        }
      },
      type: 'object'
    }],
    type: 'layout'
  }
});
module.exports = exports.default;
//# sourceMappingURL=checkLineAlignment.cjs.map