"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _iterateJsdoc = _interopRequireDefault(require("../iterateJsdoc.cjs"));
var _escapeStringRegexp = _interopRequireDefault(require("escape-string-regexp"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const otherDescriptiveTags = new Set(['classdesc', 'deprecated', 'exception', 'file', 'fileoverview', 'overview',
// 'copyright' and 'see' might be good addition, but as the former may be
//   sensitive text, and the latter may have just a link, they are not
//   included by default
'summary', 'throws', 'todo', 'yield', 'yields']);

/**
 * @param {string} text
 * @returns {string[]}
 */
const extractParagraphs = text => {
  return text.split(/(?<![;:])\n\n+/v);
};

/**
 * @param {string} text
 * @param {string|RegExp} abbreviationsRegex
 * @returns {string[]}
 */
const extractSentences = (text, abbreviationsRegex) => {
  const txt = text
  // Remove all {} tags.
  .replaceAll(/(?<!^)\{[\s\S]*?\}\s*/gv, '')

  // Remove custom abbreviations
  .replace(abbreviationsRegex, '');
  const sentenceEndGrouping = /([.?!])(?:\s+|$)/gv;
  const puncts = [...txt.matchAll(sentenceEndGrouping)].map(sentEnd => {
    return sentEnd[0];
  });
  return txt.split(/[.?!](?:\s+|$)/v)

  // Re-add the dot.
  .map((sentence, idx) => {
    return !puncts[idx] && /^\s*$/v.test(sentence) ? sentence : `${sentence}${puncts[idx] || ''}`;
  });
};

/**
 * @param {string} text
 * @returns {boolean}
 */
const isNewLinePrecededByAPeriod = text => {
  /** @type {boolean} */
  let lastLineEndsSentence;
  const lines = text.split('\n');
  return !lines.some(line => {
    if (lastLineEndsSentence === false && /^[A-Z][a-z]/v.test(line)) {
      return true;
    }
    lastLineEndsSentence = /[.:?!\|]$/v.test(line);
    return false;
  });
};

/**
 * @param {string} str
 * @returns {boolean}
 */
const isCapitalized = str => {
  return str[0] === str[0].toUpperCase();
};

/**
 * @param {string} str
 * @returns {boolean}
 */
const isTable = str => {
  return str.charAt(0) === '|';
};

/**
 * @param {string} str
 * @returns {string}
 */
const capitalize = str => {
  return str.charAt(0).toUpperCase() + str.slice(1);
};

/**
 * @param {string} description
 * @param {import('../iterateJsdoc.js').Report} reportOrig
 * @param {import('eslint').Rule.Node} jsdocNode
 * @param {string|RegExp} abbreviationsRegex
 * @param {import('eslint').SourceCode} sourceCode
 * @param {import('comment-parser').Spec|{
 *   line: import('../iterateJsdoc.js').Integer
 * }} tag
 * @param {boolean} newlineBeforeCapsAssumesBadSentenceEnd
 * @returns {boolean}
 */
const validateDescription = (description, reportOrig, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd) => {
  if (!description || /^\n+$/v.test(description)) {
    return false;
  }
  const descriptionNoHeadings = description.replaceAll(/^\s*#[^\n]*(\n|$)/gmv, '');
  const paragraphs = extractParagraphs(descriptionNoHeadings).filter(Boolean);
  return paragraphs.some((paragraph, parIdx) => {
    const sentences = extractSentences(paragraph, abbreviationsRegex);
    const fix = /** @type {import('eslint').Rule.ReportFixer} */fixer => {
      let text = sourceCode.getText(jsdocNode);
      if (!/[.:?!]$/v.test(paragraph)) {
        const line = paragraph.split('\n').findLast(Boolean);
        text = text.replace(new RegExp(`${(0, _escapeStringRegexp.default)(/** @type {string} */
        line)}$`, 'mv'), `${line}.`);
      }
      for (const sentence of sentences.filter(sentence_ => {
        return !/^\s*$/v.test(sentence_) && !isCapitalized(sentence_) && !isTable(sentence_);
      })) {
        const beginning = sentence.split('\n')[0];
        if ('tag' in tag && tag.tag) {
          const reg = new RegExp(`(@${(0, _escapeStringRegexp.default)(tag.tag)}.*)${(0, _escapeStringRegexp.default)(beginning)}`, 'v');
          text = text.replace(reg, (_$0, $1) => {
            return $1 + capitalize(beginning);
          });
        } else {
          text = text.replace(new RegExp('((?:[.?!]|\\*|\\})\\s*)' + (0, _escapeStringRegexp.default)(beginning), 'v'), '$1' + capitalize(beginning));
        }
      }
      return fixer.replaceText(jsdocNode, text);
    };

    /**
     * @param {string} msg
     * @param {import('eslint').Rule.ReportFixer | null | undefined} fixer
     * @param {{
     *   line?: number | undefined;
     *   column?: number | undefined;
     * } | (import('comment-parser').Spec & {
     *   line?: number | undefined;
     *   column?: number | undefined;
     * })} tagObj
     * @returns {void}
     */
    const report = (msg, fixer, tagObj) => {
      if ('line' in tagObj) {
        /**
         * @type {{
         *   line: number;
         * }}
         */
        tagObj.line += parIdx * 2;
      } else {
        /** @type {import('comment-parser').Spec} */tagObj.source[0].number += parIdx * 2;
      }

      // Avoid errors if old column doesn't exist here
      tagObj.column = 0;
      reportOrig(msg, fixer, tagObj);
    };
    if (sentences.some(sentence => {
      return /^[.?!]$/v.test(sentence);
    })) {
      report('Sentences must be more than punctuation.', null, tag);
    }
    if (sentences.some(sentence => {
      return !/^\s*$/v.test(sentence) && !isCapitalized(sentence) && !isTable(sentence);
    })) {
      report('Sentences should start with an uppercase character.', fix, tag);
    }
    const paragraphNoAbbreviations = paragraph.replace(abbreviationsRegex, '');
    if (!/(?:[.?!\|]|```)\s*$/v.test(paragraphNoAbbreviations)) {
      report('Sentences must end with a period.', fix, tag);
      return true;
    }
    if (newlineBeforeCapsAssumesBadSentenceEnd && !isNewLinePrecededByAPeriod(paragraphNoAbbreviations)) {
      report('A line of text is started with an uppercase character, but the preceding line does not end the sentence.', null, tag);
      return true;
    }
    return false;
  });
};
var _default = exports.default = (0, _iterateJsdoc.default)(({
  context,
  jsdoc,
  jsdocNode,
  report,
  sourceCode,
  utils
}) => {
  const /** @type {{abbreviations: string[], newlineBeforeCapsAssumesBadSentenceEnd: boolean}} */{
    abbreviations = [],
    newlineBeforeCapsAssumesBadSentenceEnd = false
  } = context.options[0] || {};

  // `@inheritDoc` can be used as inline tag with TSDoc/typedoc: https://typedoc.org/documents/Tags.__inheritDoc_.html
  if (utils.getInlineTags().some(({
    tag
  }) => {
    return [
    // Typdoc
    'include', 'includeCode',
    // TSDoc
    'inheritDoc', 'inheritdoc', 'label'].includes(tag);
  })) {
    return;
  }
  const abbreviationsRegex = abbreviations.length ? new RegExp('\\b' + abbreviations.map(abbreviation => {
    return (0, _escapeStringRegexp.default)(abbreviation.replaceAll(/\.$/gv, '') + '.');
  }).join('|') + '(?:$|\\s)', 'gv') : '';
  let {
    description
  } = utils.getDescription();
  const indices = [...description.matchAll(/```[\s\S]*```/gv)].map(match => {
    const {
      index
    } = match;
    const [{
      length
    }] = match;
    return {
      index,
      length
    };
  }).toReversed();
  for (const {
    index,
    length
  } of indices) {
    description = description.slice(0, index) + description.slice(/** @type {import('../iterateJsdoc.js').Integer} */index + length);
  }
  if (validateDescription(description, report, jsdocNode, abbreviationsRegex, sourceCode, {
    line: jsdoc.source[0].number + 1
  }, newlineBeforeCapsAssumesBadSentenceEnd)) {
    return;
  }
  utils.forEachPreferredTag('description', matchingJsdocTag => {
    const desc = `${matchingJsdocTag.name} ${utils.getTagDescription(matchingJsdocTag)}`.trim();
    validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, matchingJsdocTag, newlineBeforeCapsAssumesBadSentenceEnd);
  }, true);
  const {
    tagsWithNames
  } = utils.getTagsByType(jsdoc.tags);
  const tagsWithoutNames = utils.filterTags(({
    tag: tagName
  }) => {
    return otherDescriptiveTags.has(tagName) || utils.hasOptionTag(tagName) && !tagsWithNames.some(({
      tag
    }) => {
      // If user accidentally adds tags with names (or like `returns`
      //  get parsed as having names), do not add to this list
      return tag === tagName;
    });
  });
  tagsWithNames.some(tag => {
    const desc = /** @type {string} */utils.getTagDescription(tag).replace(/^- /v, '').trimEnd();
    return validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd);
  });
  tagsWithoutNames.some(tag => {
    const desc = `${tag.name} ${utils.getTagDescription(tag)}`.trim();
    return validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd);
  });
}, {
  iterateAllJsdocs: true,
  meta: {
    docs: {
      description: 'Requires that block description, explicit `@description`, and `@param`/`@returns` tag descriptions are written in complete sentences.',
      url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-description-complete-sentence.md#repos-sticky-header'
    },
    fixable: 'code',
    schema: [{
      additionalProperties: false,
      properties: {
        abbreviations: {
          description: `You can provide an \`abbreviations\` options array to avoid such strings of text
being treated as sentence endings when followed by dots. The \`.\` is not
necessary at the end of the array items.`,
          items: {
            type: 'string'
          },
          type: 'array'
        },
        newlineBeforeCapsAssumesBadSentenceEnd: {
          description: `When \`false\` (the new default), we will not assume capital letters after
newlines are an incorrect way to end the sentence (they may be proper
nouns, for example).`,
          type: 'boolean'
        },
        tags: {
          description: `If you want additional tags to be checked for their descriptions, you may
add them within this option.

\`\`\`js
{
  'jsdoc/require-description-complete-sentence': ['error', {
    tags: ['see', 'copyright']
  }]
}
\`\`\`

The tags \`@param\`/\`@arg\`/\`@argument\` and \`@property\`/\`@prop\` will be properly
parsed to ensure that the checked "description" text includes only the text
after the name.

All other tags will treat the text following the tag name, a space, and
an optional curly-bracketed type expression (and another space) as part of
its "description" (e.g., for \`@returns {someType} some description\`, the
description is \`some description\` while for \`@some-tag xyz\`, the description
is \`xyz\`).`,
          items: {
            type: 'string'
          },
          type: 'array'
        }
      },
      type: 'object'
    }],
    type: 'suggestion'
  }
});
module.exports = exports.default;
//# sourceMappingURL=requireDescriptionCompleteSentence.cjs.map