001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.javadoc;
021
022import java.util.ArrayList;
023import java.util.List;
024import java.util.regex.Pattern;
025
026import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailNode;
028import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
029import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
030
031/**
032 * <div>
033 * Checks that Javadoc lines efficiently utilize the available horizontal space.
034 * </div>
035 *
036 * <p>
037 * Every line of a Javadoc comment is compared with {@code lineLimit}, the preferred line
038 * length. A line is reported when it is longer than the limit, or when it stops early although
039 * the next line would still have fit on it. Blank lines are never reported.
040 * The examples on this page use the default limit of 80 characters, and show the Javadoc
041 * of a method, indented by four spaces.
042 * </p>
043 *
044 * <p>
045 * <b>Lines that stop too early.</b> A line is reported when the whole next line would
046 * still fit on it. The fix is to join the two lines, so the comment gets one line shorter.
047 * When only a part of the next line would fit, the line is not reported, because moving
048 * words up would not make the comment shorter. Lines are never joined with a line that is
049 * blank, or that starts with a block tag, an inline tag, a URL or an HTML tag, so the line
050 * before such a line is not reported.
051 * </p>
052 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
053 *     // violation, the second line would fit on the first line
054 *     &#47;**
055 *      * Not part of the API. This is returned when a type
056 *      * is not known.
057 *      *&#47;
058 *
059 *     // ok, only a part of the second line would fit on the first line
060 *     &#47;**
061 *      * Not part of the API. This is returned when a type
062 *      * not known to this wrapper is returned.
063 *      *&#47;
064 * </code></pre></div>
065 *
066 * <p>
067 * <b>Lines that could take more words.</b> When {@code validateOnlyJoinableLines} is set
068 * to {@code false}, words are expected to fill a line up to the limit, so a line is also
069 * reported when only the first word of the next line would still fit on it. The fix is to
070 * move words up until the next word does not fit.
071 * </p>
072 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
073 *     // violation, "not" would fit on the first line
074 *     &#47;**
075 *      * Not part of the API. This is returned when a type
076 *      * not known to this wrapper is returned.
077 *      *&#47;
078 *
079 *     // ok
080 *     &#47;**
081 *      * Not part of the API. This is returned when a type not known to this
082 *      * wrapper is returned.
083 *      *&#47;
084 * </code></pre></div>
085 *
086 * <p>
087 * <b>Lines that end with punctuation.</b> A line that matches {@code ignoreTooShortPattern}
088 * is not reported as stopping too early, and no words are moved up to it. By default this is
089 * every line that ends with a period, a colon or a comma, so a sentence, or a part of it,
090 * can end before the limit. Trailing whitespace of the line is ignored when the pattern is
091 * applied. The line after such a line is checked on its own. Such lines can still be
092 * reported as too long.
093 * </p>
094 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
095 *     // ok, the first line ends with a period
096 *     &#47;**
097 *      * Not part of the API.
098 *      * This is returned when a type not known to this wrapper is returned.
099 *      *&#47;
100 *
101 *     // violation, the third line would fit on the second line
102 *     &#47;**
103 *      * Not part of the API.
104 *      * This is returned when a type not
105 *      * known to this wrapper is returned.
106 *      *&#47;
107 * </code></pre></div>
108 *
109 * <p>
110 * <b>Lines that are too long.</b> A line is reported when it is longer than the limit.
111 * The fix is to wrap it at a word.
112 * </p>
113 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
114 *     // violation, longer than 80 characters
115 *     &#47;**
116 *      * Creates a client for the service and loads its settings from the given file path.
117 *      *&#47;
118 *
119 *     // ok
120 *     &#47;**
121 *      * Creates a client for the service and loads its settings from the given
122 *      * file path.
123 *      *&#47;
124 * </code></pre></div>
125 *
126 * <p>
127 * <b>Block tags.</b> The value of a block tag, such as {@code @param}, {@code @return}
128 * or {@code @throws}, is checked like any other text. A line that has only the name of
129 * the block tag is not reported, so the value can start on the next line. A line that
130 * starts with a block tag is never joined to the line before it.
131 * </p>
132 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
133 *     // violation, longer than 80 characters
134 *     &#47;**
135 *      * &#64;return Date the company was closed or dissolved, see {&#64;link #getStatus()}.
136 *      *&#47;
137 *
138 *     // ok, the value starts on the next line
139 *     &#47;**
140 *      * &#64;return
141 *      *     Date the company was closed or dissolved, see {&#64;link #getStatus()}.
142 *      *&#47;
143 *
144 *     // ok, wrapped before the inline tag
145 *     &#47;**
146 *      * &#64;return Date the company was closed or dissolved, see
147 *      * {&#64;link #getStatus()}.
148 *      *&#47;
149 * </code></pre></div>
150 *
151 * <p>
152 * <b>Inline tags.</b> An inline tag, such as a link to a class, cannot be wrapped. A line
153 * that starts with an inline tag and has nothing else on it is not reported, even when it is
154 * longer than the limit. When other words follow on the same line, the line is reported,
155 * because those words can move to the next line. A block tag never gets this exception for
156 * its value.
157 * </p>
158 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
159 *     // ok, a tag alone on a line may be longer than 80 characters
160 *     &#47;**
161 *      * Refer to the method
162 *      * {&#64;link com.example.registry.status.CompanyStatusRegistry#findStatus(String)}
163 *      *&#47;
164 *
165 *     // violation, other words follow the tag
166 *     &#47;**
167 *      * {&#64;link com.example.registry.status.CompanyStatusRegistry#findStatus(String)} finds it.
168 *      *&#47;
169 * </code></pre></div>
170 *
171 * <p>
172 * <b>URLs and links.</b> A URL cannot be wrapped, so a line that matches
173 * {@code ignorePattern} is not reported as too long. By default this is every line that has
174 * a URL or the {@code href} attribute of a link. A block tag line whose value starts with a
175 * link, such as {@code @see <a href="...">}, is therefore not reported, whether the link ends
176 * on that line or on a later one. Such lines can still be reported as stopping too early.
177 * </p>
178 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
179 *     // ok, lines that have a URL or a link may be longer than 80 characters
180 *     &#47;**
181 *      * See https://example.com/documentation/company-registry/api/v2/status-endpoints/details
182 *      *
183 *      * &#64;see &lt;a href="https://example.com/documentation/company-registry/api/v2/status"&gt;
184 *      *     Status endpoints&lt;/a&gt;
185 *      *&#47;
186 * </code></pre></div>
187 *
188 * <p>
189 * <b>HTML.</b> A line that starts with an HTML tag, such as {@code <p>} or {@code </li>}, is
190 * not reported as too short, and it is not joined to the line before it, so tags can be
191 * placed on their own lines. Such a line, and the text inside an HTML element, must still
192 * not be longer than the limit.
193 * </p>
194 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
195 *     // ok, a line that starts with an HTML tag is never too short
196 *     &#47;**
197 *      * &lt;p&gt;Companies House allows you to query
198 *      * information on registered companies in Britain.&lt;/p&gt;
199 *      *&#47;
200 *
201 *     // violation, text inside an HTML element is checked
202 *     &#47;**
203 *      * &lt;ul&gt;
204 *      * &lt;li&gt;Static configuration reads files from the classpath or a given path.&lt;/li&gt;
205 *      * &lt;/ul&gt;
206 *      *&#47;
207 *
208 *     // ok
209 *     &#47;**
210 *      * &lt;ul&gt;
211 *      * &lt;li&gt;Static configuration reads files from the classpath or a given
212 *      * path.&lt;/li&gt;
213 *      * &lt;/ul&gt;
214 *      *&#47;
215 * </code></pre></div>
216 *
217 * <p>
218 * <b>Pre and code elements.</b> The content of {@code <pre>} and {@code <code>} elements,
219 * and HTML comments, is ignored, so code samples can keep their own formatting.
220 * </p>
221 * <div class="wrapper"><pre class="prettyprint"><code class="language-java">
222 *     // ok, the content of a pre element is ignored
223 *     &#47;**
224 *      * Example:
225 *      * &lt;pre&gt;
226 *      * new CompanyRegistry().findStatus("00000006").orElseThrow(NotRegisteredException::new);
227 *      * &lt;/pre&gt;
228 *      *&#47;
229 * </code></pre></div>
230 *
231 * @since 14.4.0
232 */
233@FileStatefulCheck
234public class JavadocUtilizingTrailingSpaceCheck extends AbstractJavadocCheck {
235
236    /** Message key for too short Javadoc line. */
237    public static final String MSG_TOO_SHORT = "javadoc.utilizing.trailing.space.too.short";
238
239    /** Message key for too long Javadoc line. */
240    public static final String MSG_TOO_LONG = "javadoc.utilizing.trailing.space.too.long";
241
242    /** Pattern that recognizes URLs. */
243    private static final Pattern URL_PATTERN = Pattern.compile("https?://|ftp://");
244
245    /** Pattern for the indentation and the leading asterisk of a Javadoc line. */
246    private static final Pattern LINE_PREFIX = Pattern.compile("^\\s*\\*?\\s*");
247
248    /** Default maximum line length. */
249    private static final int DEFAULT_LINE_LIMIT = 80;
250
251    /** Collects logical Javadoc lines for the current Javadoc comment. */
252    private final List<JavadocLine> lines = new ArrayList<>();
253
254    /** Configurable line length limit. */
255    private int lineLimit = DEFAULT_LINE_LIMIT;
256
257    /** Pattern for lines that are not reported as too long. */
258    private Pattern ignorePattern =
259            Pattern.compile("href\\s*=\\s*\"[^\"]*\"|http://|https://|ftp://");
260
261    /** Pattern for lines that are not reported as too short. */
262    private Pattern ignoreTooShortPattern = Pattern.compile("[.:,]$");
263
264    /** Control whether a line is reported as too short only when the whole next line fits on it. */
265    private boolean validateOnlyJoinableLines = true;
266
267    /** Current line being built. */
268    private JavadocLine currentLine;
269
270    /** The node whose subtree is currently being skipped, or null. */
271    private DetailNode skipRoot;
272
273    /**
274     * Creates a new {@code JavadocUtilizingTrailingSpaceCheck} instance.
275     */
276    public JavadocUtilizingTrailingSpaceCheck() {
277        // no code by default
278    }
279
280    @Override
281    public int[] getDefaultJavadocTokens() {
282        return new int[] {
283            JavadocCommentsTokenTypes.NEWLINE,
284            JavadocCommentsTokenTypes.TEXT,
285            JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG,
286            JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG,
287            JavadocCommentsTokenTypes.HTML_ELEMENT,
288            JavadocCommentsTokenTypes.HTML_COMMENT,
289            JavadocCommentsTokenTypes.HTML_TAG_START,
290            JavadocCommentsTokenTypes.HTML_TAG_END,
291            JavadocCommentsTokenTypes.VOID_ELEMENT,
292        };
293    }
294
295    @Override
296    public int[] getRequiredJavadocTokens() {
297        return getDefaultJavadocTokens();
298    }
299
300    /**
301     * Setter to specify the line length limit.
302     *
303     * @param limit the maximum length to target for Javadoc lines
304     * @since 14.4.0
305     */
306    public void setLineLimit(int limit) {
307        lineLimit = limit;
308    }
309
310    /**
311     * Setter to specify pattern for lines that are not reported as too long.
312     *
313     * @param pattern a pattern.
314     * @since 14.4.0
315     */
316    public void setIgnorePattern(Pattern pattern) {
317        ignorePattern = pattern;
318    }
319
320    /**
321     * Setter to specify pattern for lines that are not reported as too short.
322     *
323     * @param pattern a pattern.
324     * @since 14.4.0
325     */
326    public void setIgnoreTooShortPattern(Pattern pattern) {
327        ignoreTooShortPattern = pattern;
328    }
329
330    /**
331     * Setter to control whether a line is reported as too short only when the whole next
332     * line fits on it.
333     *
334     * @param validateOnlyJoinableLines true if a line is reported as too short only when
335     *     the whole next line fits on it, false if the first word of the next line is enough.
336     * @since 14.4.0
337     */
338    public void setValidateOnlyJoinableLines(boolean validateOnlyJoinableLines) {
339        this.validateOnlyJoinableLines = validateOnlyJoinableLines;
340    }
341
342    @Override
343    public void beginJavadocTree(DetailNode rootAst) {
344        lines.clear();
345        currentLine = null;
346    }
347
348    @Override
349    public void finishJavadocTree(DetailNode rootAst) {
350        commitCurrentLine();
351        validateLines();
352    }
353
354    @Override
355    public void visitJavadocToken(DetailNode ast) {
356        if (skipRoot == null) {
357            switch (ast.getType()) {
358                case JavadocCommentsTokenTypes.NEWLINE -> handleNewline(ast);
359                case JavadocCommentsTokenTypes.TEXT -> handleText(ast);
360                case JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG -> handleBlockTag(ast);
361                case JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG -> handleInlineTag(ast);
362                case JavadocCommentsTokenTypes.HTML_ELEMENT -> handleHtmlElement(ast);
363                case JavadocCommentsTokenTypes.HTML_COMMENT -> skipNode(ast);
364                default -> handleHtmlTag(ast);
365            }
366        }
367    }
368
369    @Override
370    public void leaveJavadocToken(DetailNode ast) {
371        if (ast == skipRoot) {
372            skipRoot = null;
373        }
374    }
375
376    /**
377     * Validates all collected lines for violations.
378     */
379    private void validateLines() {
380        for (int index = 0; index < lines.size(); index++) {
381            final JavadocLine line = lines.get(index);
382
383            if (!line.hasContent) {
384                continue;
385            }
386
387            if (line.length > lineLimit && !line.isOnlyInlineTag()
388                    && !ignorePattern.matcher(getLine(line.lineNumber - 1)).find()) {
389                log(line.firstNode, MSG_TOO_LONG, lineLimit, line.length);
390            }
391
392            if (isTooShort(index)) {
393                log(line.firstNode, MSG_TOO_SHORT, lineLimit, line.length);
394            }
395        }
396    }
397
398    /**
399     * Decides whether the line at the given index is too short
400     * (i.e. we should have pulled content from the next line).
401     *
402     * @param index the line index to check
403     * @return true if the line is too short
404     */
405    private boolean isTooShort(int index) {
406        return !lines.get(index).startsWithHtml
407                && canPullFromNextLine(index)
408                && (index == 0
409                    || !lines.get(index - 1).hasContent
410                    || !canPullFromNextLine(index - 1));
411    }
412
413    /**
414     * Handles a newline token.
415     *
416     * @param node the newline node
417     */
418    private void handleNewline(DetailNode node) {
419        initCurrentLine(node.getLineNumber());
420    }
421
422    /**
423     * Handles a Javadoc block tag node. A line that starts a block tag cannot
424     * be merged into the previous line, but its value is still validated.
425     *
426     * @param node the block tag node
427     */
428    private void handleBlockTag(DetailNode node) {
429        initCurrentLine(node.getLineNumber());
430        currentLine.startsWithBlockTag = true;
431        currentLine.firstNode = node;
432    }
433
434    /**
435     * Handles a TEXT node.
436     *
437     * @param node the text node
438     */
439    private void handleText(DetailNode node) {
440        final String text = node.getText();
441
442        initCurrentLine(node.getLineNumber());
443        currentLine.updateLength(node.getColumnNumber(), text.length());
444
445        final String trimmed = text.trim();
446        if (!trimmed.isEmpty()) {
447            if (currentLine.hasContent) {
448                currentLine.markAdditionalContent(text);
449            }
450            else {
451                currentLine.initContent(trimmed, URL_PATTERN, node);
452            }
453        }
454    }
455
456    /**
457     * Handles a Javadoc inline tag node. The tag is measured as a whole and its content is skipped.
458     *
459     * @param node the inline tag node
460     */
461    private void handleInlineTag(DetailNode node) {
462        initCurrentLine(node.getLineNumber());
463        updateLengthToEnd(node);
464
465        if (currentLine.hasContent) {
466            currentLine.hasAdditionalContent = true;
467        }
468        else {
469            currentLine.hasContent = true;
470            if (!currentLine.startsWithBlockTag) {
471                currentLine.startsWithUnbreakable = true;
472                currentLine.startsWithInlineTag = true;
473                currentLine.firstNode = node;
474            }
475        }
476        skipRoot = node;
477    }
478
479    /**
480     * Handles an HTML element. The content of {@code pre} and {@code code} elements is skipped.
481     *
482     * @param element the HTML element node
483     */
484    private void handleHtmlElement(DetailNode element) {
485        if (JavadocUtil.isTag(element, "pre") || JavadocUtil.isTag(element, "code")) {
486            skipNode(element);
487        }
488    }
489
490    /**
491     * Handles an HTML start tag, end tag or void element. A line that starts
492     * with such a tag is never reported as too short, but it can still be too long.
493     *
494     * @param node the HTML tag node
495     */
496    private void handleHtmlTag(DetailNode node) {
497        initCurrentLine(node.getLineNumber());
498        if (!currentLine.hasContent) {
499            currentLine.hasContent = true;
500            if (!currentLine.startsWithBlockTag) {
501                currentLine.startsWithHtml = true;
502                currentLine.firstNode = node;
503            }
504        }
505        updateLengthToEnd(node);
506        skipRoot = node;
507    }
508
509    /**
510     * Skips the subtree of the given node. If the node spans several lines, the
511     * line where it starts is marked, because the lines after it are not adjacent
512     * to the lines that were skipped.
513     *
514     * @param node the node to skip
515     */
516    private void skipNode(DetailNode node) {
517        initCurrentLine(node.getLineNumber());
518
519        final DetailNode end = lastLeaf(node);
520        if (end.getLineNumber() == node.getLineNumber()) {
521            currentLine.updateLength(end.getColumnNumber(), end.getText().length());
522        }
523        else {
524            currentLine.skippedLinesFollow = true;
525        }
526        skipRoot = node;
527    }
528
529    /**
530     * Extends the current line to the end of the given node, if the node ends on
531     * the line where it starts.
532     *
533     * @param node the node to measure
534     */
535    private void updateLengthToEnd(DetailNode node) {
536        final DetailNode end = lastLeaf(node);
537        if (end.getLineNumber() == node.getLineNumber()) {
538            currentLine.updateLength(end.getColumnNumber(), end.getText().length());
539        }
540    }
541
542    /**
543     * Finds the last leaf of the subtree of the given node.
544     *
545     * @param node the root of the subtree
546     * @return the last leaf, which is the node itself if it has no children
547     */
548    private static DetailNode lastLeaf(DetailNode node) {
549        DetailNode leaf = node;
550        DetailNode child = leaf.getFirstChild();
551        while (child != null) {
552            while (child.getNextSibling() != null) {
553                child = child.getNextSibling();
554            }
555            leaf = child;
556            child = leaf.getFirstChild();
557        }
558        return leaf;
559    }
560
561    /**
562     * Checks if content can be pulled from the next line.
563     *
564     * @param currentIndex current line index
565     * @return true if the content to pull from the next content line fits within lineLimit
566     */
567    private boolean canPullFromNextLine(int currentIndex) {
568        boolean pullNext = false;
569
570        final JavadocLine line = lines.get(currentIndex);
571
572        if (!line.skippedLinesFollow && currentIndex + 1 < lines.size()
573                && !ignoreTooShortPattern.matcher(
574                    getLine(line.lineNumber - 1).stripTrailing()).find()) {
575
576            final JavadocLine nextLine = lines.get(currentIndex + 1);
577
578            if (nextLine.hasContent && !nextLine.startsWithUnbreakable
579                    && !nextLine.startsWithBlockTag && !nextLine.startsWithHtml) {
580                final int potentialLength = line.length + 1 + getPullLength(nextLine);
581                pullNext = potentialLength <= lineLimit;
582            }
583        }
584
585        return pullNext;
586    }
587
588    /**
589     * Returns the length of the content that has to fit on the line before the given
590     * line, for that line to be too short.
591     *
592     * @param nextLine the line to pull content from
593     * @return the length of the whole content of the line if only joinable lines are
594     *         validated, the length of its first word otherwise
595     */
596    private int getPullLength(JavadocLine nextLine) {
597        final int pullLength;
598        if (validateOnlyJoinableLines) {
599            final String sourceLine = getLine(nextLine.lineNumber - 1);
600            final int contentStart = sourceLine.length()
601                    - LINE_PREFIX.matcher(sourceLine).replaceFirst("").length();
602            pullLength = nextLine.length - contentStart;
603        }
604        else {
605            pullLength = nextLine.firstWordLength();
606        }
607        return pullLength;
608    }
609
610    /**
611     * Initializes the current line if needed.
612     *
613     * @param lineNumber the source line number
614     */
615    private void initCurrentLine(int lineNumber) {
616        if (currentLine == null || currentLine.lineNumber != lineNumber) {
617            commitCurrentLine();
618            currentLine = new JavadocLine(lineNumber);
619        }
620    }
621
622    /**
623     * Commits the current line to the lines list and clears it.
624     */
625    private void commitCurrentLine() {
626        if (currentLine != null) {
627            lines.add(currentLine);
628        }
629    }
630
631    /**
632     * Represents a logical line in a Javadoc comment.
633     */
634    private static final class JavadocLine {
635
636        /** The source line number. */
637        private final int lineNumber;
638
639        /** The first node with content on this line, the location of violations. */
640        private DetailNode firstNode;
641
642        /** The length of the line (end column). */
643        private int length;
644
645        /** Whether the line has meaningful content. */
646        private boolean hasContent;
647
648        /** Whether the line starts with an unbreakable element (inline tag or URL). */
649        private boolean startsWithUnbreakable;
650
651        /** Whether the line starts with an inline tag. */
652        private boolean startsWithInlineTag;
653
654        /** Whether the line has content after its leading inline tag. */
655        private boolean hasAdditionalContent;
656
657        /** Whether the line starts with a Javadoc block tag. */
658        private boolean startsWithBlockTag;
659
660        /** Whether the line starts with an HTML tag. */
661        private boolean startsWithHtml;
662
663        /** Whether skipped lines follow this line, so the next line is not adjacent. */
664        private boolean skippedLinesFollow;
665
666        /** The first word of the line, used for pull calculations. */
667        private String firstWord;
668
669        /**
670         * Creates a JavadocLine.
671         *
672         * @param lineNumber the source line number
673         */
674        private JavadocLine(int lineNumber) {
675            this.lineNumber = lineNumber;
676        }
677
678        /**
679         * Updates the line length based on column position and content length.
680         *
681         * @param startColumn   the starting column (0-indexed)
682         * @param contentLength the length of the content
683         */
684        private void updateLength(int startColumn, int contentLength) {
685            length = startColumn + contentLength;
686        }
687
688        /**
689         * Initializes content state for the first non-empty text on this line.
690         *
691         * @param trimmedText the trimmed text content
692         * @param urlPattern  pattern used to detect URLs
693         * @param textNode    the node of the text content
694         */
695        private void initContent(String trimmedText, Pattern urlPattern,
696                DetailNode textNode) {
697            hasContent = true;
698            firstWord = trimmedText.split(" ", 2)[0];
699            if (!startsWithBlockTag) {
700                firstNode = textNode;
701                startsWithUnbreakable = urlPattern.matcher(trimmedText).lookingAt();
702            }
703        }
704
705        /**
706         * Marks the line as having additional content when the given text,
707         * which follows the first content of the line, contains a separate word.
708         * Text attached to the preceding element, such as trailing punctuation,
709         * is not additional content.
710         *
711         * @param text the text that follows the first content of the line
712         */
713        private void markAdditionalContent(String text) {
714            if (text.stripTrailing().chars().anyMatch(Character::isWhitespace)) {
715                hasAdditionalContent = true;
716            }
717        }
718
719        /**
720         * Checks whether the only content of this line is a single inline tag,
721         * which may exceed the limit.
722         *
723         * @return true if the line contains only an inline tag
724         */
725        private boolean isOnlyInlineTag() {
726            return startsWithInlineTag && !hasAdditionalContent;
727        }
728
729        /**
730         * Returns the length of the first word on this line.
731         *
732         * @return the first word length
733         */
734        private int firstWordLength() {
735            return firstWord.length();
736        }
737
738    }
739
740}