(defvar yk-xhtml-zeroindent-tags
(list "html"
"title" "style"
"body" "article" "section"
"h1" "h2" "h3" "h4" "h5" "h6"
"p" "pre" "figcaption" "main"
"a" "em" "strong" "small" "s" "cite" "q" "dfn" "abbr" "data" "time"
"code" "var" "samp" "kbd" "sub" "sup" "i" "b" "u" "mark"
"ruby" "rb" "rt" "rtc" "rp" "bdi" "bdo" "span"
"table" "caption"
"script")
"Tags whose elements’ content should not be indented. This
should include inline elements and may include container
elements for which indentation adds no value.")
(defvar yk-xhtml-conditional-indent-tags
(list "blockquote" "ol" "ul" "dl"
"ins" "del"
"embed" "object" "video" "audio" "map"
"td" "th"
"button")
"Tags whose elements’ content should be indented only if there
is no content on the same line with the opening tag. This should
include elements with mixed content model; they will indent if
used as block elements.")
(defvar yk-xhtml-sibling-indent-tags
'((("dd" "dt") . 1) (("dt" "dd") . -1))
"Tags which indent relative to their preceding sibling.
Format: List of triples of the form ((TAG SIBLING-TAG) . OFFSET).")
(defvar yk-xhtml-noindent-tags
(list "style" "script" "pre" "code" "textarea")
"Tags whose elements’ content should retain its indentation.")
(defun yk-xhtml--noindent-elements ()
"Return 'noindent if within a noindent element, nil if not.
Destroys: xmltok state."
(save-excursion
(back-to-indentation)
(catch 'yk-xhtml-indent-result
(while (< (point-min) (point))
(nxml-token-before)
(let ((token-start xmltok-start))
(cond
((eq xmltok-type 'start-tag)
(if (member (xmltok-start-tag-local-name) yk-xhtml-noindent-tags)
(throw 'yk-xhtml-indent-result 'noindent)
(goto-char token-start)))
((eq xmltok-type 'end-tag)
(condition-case nil
(progn
(nxml-scan-element-backward (point))
(goto-char xmltok-start))
(nxml-scan-error (goto-char token-start))))
(t
(goto-char token-start)))))
nil)))
(defun yk-xhtml-compute-indent--for-closing-tag ()
"Return indentation for a single closing tag on a line.
A closing tag indents to its opening tag if both tags are the
only non-whitespace content on their lines.
If the current line does not consist of a single closing tag,
or if there is no matching opening tag, or if there is other
content before the matching opening tag in the same line,
return nil.
Destroys: xmltok state."
(save-excursion
(back-to-indentation)
(let ((bol (point)))
(end-of-line)
(skip-chars-backward " \t")
(and ;; Current line contains only a closing tag
(= (nxml-token-before) (point))
(memq xmltok-type '(end-tag partial-end-tag))
(= xmltok-start bol)
;; No content before the matching opening tag on its line
(let ((tok-end
(condition-case nil
(nxml-scan-element-backward
(point) nil
(- (point)
nxml-end-tag-indent-scan-distance))
(nxml-scan-error nil))))
(and tok-end
;; (progn
;; (goto-char tok-end)
;; (looking-at "[ \t]*$"))
(progn
(goto-char xmltok-start)
(looking-back "^[ \t]*"))
;; If all conditions met, return opening tag indentation
(current-column)))))))
(defun yk-xhtml-compute-indent--from-preceding-sibling ()
"Return indentation relative to the preceding sibling element.
If the first token on the current line is an opening tag and the
previous line ends with a closing tag and the matching opening
tag starts a line, return the indentation of the sibling
element’s opening tag adjusted by the offset specified in
`yk-xhtml-sibling-indent-tags' multiplied by
`nxml-child-indent'. Otherwise, return nil.
Destroys: xmltok state."
(save-excursion
(catch 'yk-result
(back-to-indentation)
(nxml-token-after)
(unless (eq xmltok-type 'start-tag) (throw 'yk-result nil))
(let ((this-tag-name (xmltok-start-tag-local-name)))
(forward-line -1)
(end-of-line)
(skip-chars-backward " \t")
(nxml-token-before)
(unless (eq xmltok-type 'end-tag) (throw 'yk-result nil))
(let* ((preceding-tag-name (xmltok-end-tag-local-name))
(pair (assoc (list this-tag-name preceding-tag-name)
yk-xhtml-sibling-indent-tags)))
(unless pair (throw 'yk-result nil))
(condition-case nil
(nxml-scan-element-backward (point))
(nxml-scan-error (throw 'yk-result nil)))
(goto-char xmltok-start)
(unless (looking-back "^[ \t]*") (throw 'yk-result nil))
(+ (current-column)
(* nxml-child-indent
(cdr pair))))))))
(defun yk-xhtml-compute-indent--from-preceding-element ()
"Return indentation of the preceding block element.
If the opening tag matching the closing tag before point
starts a line, return the indentation of the opening tag.
Otherwise (if there is no matching opening tag, or if there is
non-blank text preceding it on the line), return nil.
Expects: point immediately following a closing tag.
Destroys: point, xmltok state."
(and (condition-case nil
(nxml-scan-element-backward
(point) nil
(- (point) nxml-end-tag-indent-scan-distance))
(nxml-scan-error nil))
(progn
(goto-char xmltok-start)
(skip-chars-backward " \t")
(bolp))
(progn
(goto-char xmltok-start)
(current-column))))
(defun yk-xhtml--opening-tag ()
"Analyze the opening tag after point.
Return a list of the form (TAG-NAME CLOSED MORE-TEXT), where:
* TAG-NAME is a string containing the local name of the tag.
* CLOSED is t if the element is closed before the end of line,
nil otherwise.
* MORE-TEXT is t if there is any non-whitespace following
the opening tag, nil if only whitespace.
Return nil if point is not immediately preceding an opening tag.
Destroys: xmltok state."
(let ((token-end (nxml-token-after)))
(and (= xmltok-start (point))
(eq xmltok-type 'start-tag)
(let ((tag-name (xmltok-start-tag-local-name))
(more-text (save-excursion
(goto-char token-end)
(not (looking-at "[ \t]*$")))))
(let ((closing-tag-end
(condition-case nil
(nxml-scan-element-forward (point))
(nxml-scan-error nil)))
(eol (save-excursion
(end-of-line)
(point))))
(list tag-name
(and closing-tag-end
(<= closing-tag-end eol))
more-text))))))
(defun yk-xhtml-compute-indent--from-previous-line ()
"Compute the indentation based on the previous non-blank line.
* If there is no previous line, return 0.
* If the previous line ends with a closing tag
and the corresponding opening tag starts a line,
return the indentation of the opening tag.
* If the previous line starts with text, return its indentation.
* If the previous line starts with an opening tag:
* If it is closed on the same line, or
* if it is listed in `yk-xhtml-zeroindent-tags', or
* if it is listed in `yk-xhtml-conditional-indent-tags' and
there is no other text on the same line, return its indentation.
* Otherwise, return its indentation plus `nxml-child-indent'.
If the previous line starts with an opening tag which is not
closed on the same line, return the indentation of that line plus
`nxml-child-indent'."
(save-excursion
(while (and (zerop (forward-line -1))
(looking-at "[ \t]*$")))
;; now either at the first line or at start of a non-whitespace line
(if (looking-at "[ \t]*$") ;; first line which is blank
0
(end-of-line)
(let ((eol (point)))
(skip-chars-backward " \t")
(nxml-token-before)
(or (and (eq xmltok-type 'end-tag)
(yk-xhtml-compute-indent--from-preceding-element))
(progn
(back-to-indentation)
(pcase (yk-xhtml--opening-tag)
(`(,tag-name ,closed ,more-text)
(if (or closed
(member tag-name yk-xhtml-zeroindent-tags)
(and (not more-text)
(member tag-name yk-xhtml-conditional-indent-tags)))
(current-column)
(+ (current-column) nxml-child-indent)))
(_ (current-column)))))))))
(defun yk-xhtml--compute-indent ()
"Compute indentation for the current line."
(or (yk-xhtml--noindent-elements)
(yk-xhtml-compute-indent--for-closing-tag)
(yk-xhtml-compute-indent--from-preceding-sibling)
(yk-xhtml-compute-indent--from-previous-line)
'noindent))
(defun yk-xhtml-indent-line ()
"Indent the current line suitably for XHTML."
(let ((indent (yk-xhtml--compute-indent))
(savep (> (point)
(save-excursion
(back-to-indentation)
(point)))))
(if (not (numberp indent))
indent
(if savep
(save-excursion
(indent-line-to indent))
(indent-line-to indent)))))
(defun yk-xhtml-indent--maybe-enable ()
"Set the current buffer’s indentation function
to `yk-xhtml-indent-line' if the current schema is “html”."
(and (stringp (caddr rng-current-schema))
(string= (caddr rng-current-schema) "html")
(set-variable 'indent-line-function 'yk-xhtml-indent-line 'local)))
(with-eval-after-load 'nxml-mode
(add-hook 'nxml-mode-hook 'yk-xhtml-indent--maybe-enable))
(provide 'yk-xhtml-indent)