X-Git-Url: https://code.delx.au/gnu-emacs/blobdiff_plain/b35f288d478ef137a4d9e8e5a6a5f368a86b01f5..f2cdb04ac04fb8f9f92bce11df6e4a020720208b:/lisp/nxml/nxml-mode.el diff --git a/lisp/nxml/nxml-mode.el b/lisp/nxml/nxml-mode.el index b1c6194cfa..5eb9840a4c 100644 --- a/lisp/nxml/nxml-mode.el +++ b/lisp/nxml/nxml-mode.el @@ -1,6 +1,6 @@ ;;; nxml-mode.el --- a new XML mode -;; Copyright (C) 2003, 2004, 2007, 2008 Free Software Foundation, Inc. +;; Copyright (C) 2003, 2004, 2007, 2008, 2009, 2010 Free Software Foundation, Inc. ;; Author: James Clark ;; Keywords: XML @@ -24,16 +24,13 @@ ;; See nxml-rap.el for description of parsing strategy. -;; The font locking here is independent of font-lock.el. We want to -;; do more sophisticated handling of changes and we want to use the -;; same xmltok rather than regexps for parsing so that we parse -;; consistently and correctly. - ;;; Code: (when (featurep 'mucs) (error "nxml-mode is not compatible with Mule-UCS")) +(eval-when-compile (require 'cl)) ; for assert + (require 'xmltok) (require 'nxml-enc) (require 'nxml-glyph) @@ -47,19 +44,12 @@ ;;; Customization (defgroup nxml nil - "New XML editing mode" - :group 'languages - :group 'wp) + "New XML editing mode." + :group 'languages) (defgroup nxml-faces nil "Faces for XML syntax highlighting." - :group 'nxml - :group 'font-lock-faces) - -(defcustom nxml-syntax-highlight-flag t - "*Non-nil means nxml-mode should perform syntax highlighting." - :group 'nxml - :type 'boolean) + :group 'nxml) (defcustom nxml-char-ref-display-glyph-flag t "*Non-nil means display glyph following character reference. @@ -94,20 +84,18 @@ nothing else other than that start-tag." (defcustom nxml-attribute-indent 4 "*Indentation for the attributes of an element relative to the start-tag. -This only applies when the first attribute of a tag starts a line. In other -cases, the first attribute on one line is indented the same as the first -attribute on the previous line." +This only applies when the first attribute of a tag starts a line. +In other cases, the first attribute on one line is indented the same +as the first attribute on the previous line." :group 'nxml :type 'integer) -(defvar nxml-fontify-chunk-size 500) - (defcustom nxml-bind-meta-tab-to-complete-flag (not window-system) "*Non-nil means bind M-TAB in `nxml-mode-map' to `nxml-complete'. C-return will be bound to `nxml-complete' in any case. M-TAB gets swallowed by many window systems/managers, and `documentation' will show M-TAB rather than C-return as the -binding `rng-complete' when both are bound. So it's better +binding for `nxml-complete' when both are bound. So it's better to bind M-TAB only when it will work." :group 'nxml :set (lambda (sym flag) @@ -135,7 +123,7 @@ and when the encoding declaration specifies `UTF-16'." (defcustom nxml-default-buffer-file-coding-system nil "*Default value for `buffer-file-coding-system' for a buffer for a new file. -Nil means use the default value of `buffer-file-coding-system' as normal. +A value of nil means use the default value of `buffer-file-coding-system' as normal. A buffer's `buffer-file-coding-system' affects what \\[nxml-insert-xml-declaration] inserts." :group 'nxml :type 'coding-system) @@ -273,7 +261,7 @@ This includes ths `x' in hex references." '((t (:inherit nxml-delimiter))) "Face used for the colon in attribute names." :group 'nxml-faces) - + (defface nxml-attribute-local-name '((t (:inherit font-lock-variable-name-face))) "Face used for the local name of attributes." @@ -349,7 +337,7 @@ The delimiters are ." :foreground "black" :weight - normal + normal :slant normal)) (t @@ -358,7 +346,7 @@ The delimiters are ." :foreground "black" :weight - normal + normal :slant normal))) "Face used for glyph for char references." @@ -373,7 +361,7 @@ See the function `xmltok-forward-prolog' for more information.") (defvar nxml-last-fontify-end nil "Position where fontification last ended. -Nil if the buffer changed since the last fontification.") +It is nil if the buffer changed since the last fontification.") (make-variable-buffer-local 'nxml-last-fontify-end) (defvar nxml-degraded nil @@ -426,25 +414,19 @@ reference.") (define-key map "\C-c\C-o" nxml-outline-prefix-map) (define-key map [S-mouse-2] 'nxml-mouse-hide-direct-text-content) (define-key map "/" 'nxml-electric-slash) - (define-key map [C-return] 'nxml-complete) + (define-key map [C-return] 'nxml-complete) (when nxml-bind-meta-tab-to-complete-flag (define-key map "\M-\t" 'nxml-complete)) map) "Keymap for nxml-mode.") +(defvar nxml-font-lock-keywords + '(nxml-fontify-matcher) + "Default font lock keywords for nxml-mode.") + (defsubst nxml-set-face (start end face) (when (and face (< start end)) - (put-text-property start end 'face face))) - -(defun nxml-clear-face (start end) - (remove-text-properties start end '(face nil)) - (nxml-clear-char-ref-extra-display start end)) - -(defsubst nxml-set-fontified (start end) - (put-text-property start end 'fontified t)) - -(defsubst nxml-clear-fontified (start end) - (remove-text-properties start end '(fontified nil))) + (font-lock-append-text-property start end 'face face))) ;;;###autoload (defun nxml-mode () @@ -453,12 +435,9 @@ reference.") ;; not mnemonic. "Major mode for editing XML. -Syntax highlighting is performed unless the variable -`nxml-syntax-highlight-flag' is nil. - \\[nxml-finish-element] finishes the current element by inserting an end-tag. C-c C-i closes a start-tag with `>' and then inserts a balancing end-tag -leaving point between the start-tag and end-tag. +leaving point between the start-tag and end-tag. \\[nxml-balanced-close-start-tag-block] is similar but for block rather than inline elements: the start-tag, point, and end-tag are all left on separate lines. If `nxml-slash-auto-complete-flag' is non-nil, then inserting a ` end (1+ (buffer-size))) - (setq start 1) - (setq end (1+ (buffer-size)))) - (unless nxml-degraded - (condition-case err - (save-excursion - (save-restriction - (widen) - (save-match-data - (nxml-with-invisible-motion - (nxml-with-unmodifying-text-property-changes - (nxml-after-change1 start end pre-change-length)))))) - (error - (nxml-degrade 'nxml-after-change err))))) + ; In font-lock mode, nxml-after-change1 is called via + ; nxml-extend-after-change-region instead so that the updated + ; book-keeping information is available for fontification. + (unless (or font-lock-mode nxml-degraded) + (nxml-with-degradation-on-error 'nxml-after-change + (save-excursion + (save-restriction + (widen) + (save-match-data + (nxml-with-invisible-motion + (nxml-with-unmodifying-text-property-changes + (nxml-after-change1 + start end pre-change-length))))))))) (defun nxml-after-change1 (start end pre-change-length) - (setq nxml-last-fontify-end nil) + "After-change bookkeeping. +Returns a cons cell containing a possibly-enlarged change region. +You must call `nxml-extend-region' on this expanded region to obtain +the full extent of the area needing refontification. + +For bookkeeping, call this function even when fontification is +disabled." (let ((pre-change-end (+ start pre-change-length))) (setq start (nxml-adjust-start-for-dependent-regions start end pre-change-length)) + ;; If the prolog might have changed, rescan the prolog (when (<= start - ;; Add 2 so as to include the < and following char - ;; that start the instance, since changing these - ;; can change where the prolog ends. + ;; Add 2 so as to include the < and following char that + ;; start the instance (document element), since changing + ;; these can change where the prolog ends. (+ nxml-prolog-end 2)) - ;; end must be extended to at least the end of the old prolog + ;; end must be extended to at least the end of the old prolog in + ;; case the new prolog is shorter (when (< pre-change-end nxml-prolog-end) (setq end ;; don't let end get out of range even if pre-change-length ;; is bogus (min (point-max) (+ end (- nxml-prolog-end pre-change-end))))) - (nxml-scan-prolog))) - (cond ((<= end nxml-prolog-end) - (setq end nxml-prolog-end) - (goto-char start) - ;; This is so that Emacs redisplay works - (setq start (line-beginning-position))) - ((and (<= start nxml-scan-end) - (> start (point-min)) - (nxml-get-inside (1- start))) - ;; The closing delimiter might have been removed. - ;; So we may need to redisplay from the beginning - ;; of the token. - (goto-char (1- start)) - (nxml-move-outside-backwards) - ;; This is so that Emacs redisplay works - (setq start (line-beginning-position)) - (setq end (max (nxml-scan-after-change (point) end) - end))) - (t - (goto-char start) - ;; This is both for redisplay and to move back - ;; past any incomplete opening delimiters - (setq start (line-beginning-position)) - (setq end (max (nxml-scan-after-change start end) - end)))) - (when nxml-syntax-highlight-flag - (when (>= start end) - ;; Must clear at least one char so as to trigger redisplay. - (cond ((< start (point-max)) - (setq end (1+ start))) - (t - (setq end (point-max)) - (goto-char end) - (setq start (line-beginning-position))))) - (nxml-clear-fontified start end))) - + (nxml-scan-prolog) + (setq start (point-min)))) + + (when (> end nxml-prolog-end) + (goto-char start) + (nxml-move-tag-backwards (point-min)) + (setq start (point)) + (setq end (max (nxml-scan-after-change start end) + end))) + + (nxml-debug-change "nxml-after-change1" start end) + (cons start end)) + ;;; Encodings (defun nxml-insert-xml-declaration () @@ -821,7 +798,7 @@ The XML declaration will declare an encoding depending on the buffer's (setq suitable-coding-systems (cdr suitable-coding-systems)))) ret))) -(defun nxml-choose-utf-coding-system () +(defun nxml-choose-utf-coding-system () (let ((cur (and (local-variable-p 'buffer-file-coding-system) buffer-file-coding-system (coding-system-base buffer-file-coding-system)))) @@ -854,51 +831,99 @@ The XML declaration will declare an encoding depending on the buffer's ;;; Fontification -(defun nxml-fontify (start) - (condition-case err - (save-excursion - (save-restriction - (widen) - (save-match-data - (nxml-with-invisible-motion - (nxml-with-unmodifying-text-property-changes - (if (or nxml-degraded - ;; just in case we get called in the wrong buffer - (not nxml-prolog-end)) - (nxml-set-fontified start (point-max)) - (nxml-fontify1 start))))))) - (error - (nxml-degrade 'nxml-fontify err)))) - -(defun nxml-fontify1 (start) - (cond ((< start nxml-prolog-end) - (nxml-fontify-prolog) - (nxml-set-fontified (point-min) - nxml-prolog-end)) - (t - (goto-char start) - (when (not (eq nxml-last-fontify-end start)) - (when (not (equal (char-after) ?\<)) - (search-backward "<" nxml-prolog-end t)) - (nxml-ensure-scan-up-to-date) - (nxml-move-outside-backwards)) - (let ((start (point))) - (nxml-do-fontify (min (point-max) - (+ start nxml-fontify-chunk-size))) - (setq nxml-last-fontify-end (point)) - (nxml-set-fontified start nxml-last-fontify-end))))) - -(defun nxml-fontify-buffer () - (interactive) - (save-excursion - (save-restriction - (widen) - (nxml-with-invisible-motion - (goto-char (point-min)) - (nxml-with-unmodifying-text-property-changes - (nxml-fontify-prolog) - (goto-char nxml-prolog-end) - (nxml-do-fontify)))))) +(defun nxml-unfontify-region (start end) + (font-lock-default-unfontify-region start end) + (nxml-clear-char-ref-extra-display start end)) + +(defvar font-lock-beg) (defvar font-lock-end) +(defun nxml-extend-region () + "Extend the region to hold the minimum area we can fontify with nXML. +Called with `font-lock-beg' and `font-lock-end' dynamically bound." + (let ((start font-lock-beg) + (end font-lock-end)) + + (nxml-debug-change "nxml-extend-region(input)" start end) + + (when (< start nxml-prolog-end) + (setq start (point-min))) + + (cond ((<= end nxml-prolog-end) + (setq end nxml-prolog-end)) + + (t + (goto-char start) + ;; some font-lock backends (like Emacs 22 jit-lock) snap + ;; the region to the beginning of the line no matter what + ;; we say here. To mitigate the resulting excess + ;; fontification, ignore leading whitespace. + (skip-syntax-forward " ") + + ;; find the beginning of the previous tag + (when (not (equal (char-after) ?\<)) + (search-backward "<" nxml-prolog-end t)) + (nxml-ensure-scan-up-to-date) + (nxml-move-outside-backwards) + (setq start (point)) + + (while (< (point) end) + (nxml-tokenize-forward)) + + (setq end (point)))) + + (when (or (< start font-lock-beg) + (> end font-lock-end)) + (setq font-lock-beg start + font-lock-end end) + (nxml-debug-change "nxml-extend-region" start end) + t))) + +(defun nxml-extend-after-change-region (start end pre-change-length) + (unless nxml-degraded + (setq nxml-last-fontify-end nil) + (let ((region (nxml-with-degradation-on-error + 'nxml-extend-after-change-region + (save-excursion + (save-restriction + (widen) + (save-match-data + (nxml-with-invisible-motion + (nxml-with-unmodifying-text-property-changes + (nxml-extend-after-change-region1 + start end pre-change-length))))))))) + (if (consp region) region)))) + +(defun nxml-extend-after-change-region1 (start end pre-change-length) + (let* ((region (nxml-after-change1 start end pre-change-length)) + (font-lock-beg (car region)) + (font-lock-end (cdr region))) + + (nxml-extend-region) + (cons font-lock-beg font-lock-end))) + +(defun nxml-fontify-matcher (bound) + "Called as font-lock keyword matcher." + + (unless nxml-degraded + (nxml-debug-change "nxml-fontify-matcher" (point) bound) + + (when (< (point) nxml-prolog-end) + ;; prolog needs to be fontified in one go, and + ;; nxml-extend-region makes sure we start at BOB. + (assert (bobp)) + (nxml-fontify-prolog) + (goto-char nxml-prolog-end)) + + (let (xmltok-dependent-regions + xmltok-errors) + (while (and (nxml-tokenize-forward) + (<= (point) bound)) ; intervals are open-ended + (nxml-apply-fontify-rule))) + + (setq nxml-last-fontify-end (point))) + + ;; Since we did the fontification internally, tell font-lock to not + ;; do anything itself. + nil) (defun nxml-fontify-prolog () "Fontify the prolog. @@ -906,7 +931,6 @@ The buffer is assumed to be prepared for fontification. This does not set the fontified property, but it does clear faces appropriately." (let ((regions nxml-prolog-regions)) - (nxml-clear-face (point-min) nxml-prolog-end) (while regions (let ((region (car regions))) (nxml-apply-fontify-rule (aref region 0) @@ -914,17 +938,6 @@ faces appropriately." (aref region 2))) (setq regions (cdr regions))))) -(defun nxml-do-fontify (&optional bound) - "Fontify at least as far as bound. -Leave point after last fontified position." - (unless bound (setq bound (point-max))) - (let (xmltok-dependent-regions - xmltok-errors) - (while (and (< (point) bound) - (nxml-tokenize-forward)) - (nxml-clear-face xmltok-start (point)) - (nxml-apply-fontify-rule)))) - ;; Vectors identify a substring of the token to be highlighted in some face. ;; Token types returned by xmltok-forward. @@ -1240,31 +1253,33 @@ No extra whitespace is inserted." (defun nxml-balanced-close-start-tag (block-or-inline) (let ((token-end (nxml-token-before)) - (pos (1+ (point)))) + (pos (1+ (point))) + (token-start xmltok-start)) (unless (or (eq xmltok-type 'partial-start-tag) (and (memq xmltok-type '(start-tag empty-element partial-empty-element)) (>= token-end pos))) (error "Not in a start-tag")) + ;; Note that this insertion changes xmltok-start. (insert ">") (if (eq block-or-inline 'inline) (goto-char pos) - (goto-char xmltok-start) + (goto-char token-start) (back-to-indentation) - (if (= (point) xmltok-start) + (if (= (point) token-start) (let ((indent (current-column))) - (goto-char pos) - (insert "\n") - (indent-line-to indent) - (goto-char pos) - (insert "\n") - (indent-line-to (+ nxml-child-indent indent))) + (goto-char pos) + (insert "\n") + (indent-line-to indent) + (goto-char pos) + (insert "\n") + (indent-line-to (+ nxml-child-indent indent))) (goto-char pos))))) - + (defun nxml-finish-element () "Finish the current element by inserting an end-tag." (interactive "*") @@ -1356,7 +1371,8 @@ of the inserted start-tag or nil if none was inserted." "Indent current line as XML." (let ((indent (nxml-compute-indent)) (from-end (- (point-max) (point)))) - (when indent + (when (and indent + (/= indent (current-indentation))) (beginning-of-line) (let ((bol (point))) (skip-chars-forward " \t") @@ -1466,8 +1482,8 @@ its line. Otherwise return nil." (defun nxml-merge-indent-context-type (context) "Merge the indent context type CONTEXT with the token in `xmltok-type'. Return the merged indent context type. An indent context type is -either nil or one of the symbols start-tag, end-tag, markup, comment, -mixed." +either nil or one of the symbols `start-tag', `end-tag', `markup', +`comment', `mixed'." (cond ((memq xmltok-type '(start-tag partial-start-tag)) (if (memq context '(nil start-tag comment)) 'start-tag @@ -1559,7 +1575,7 @@ xmltok-* variables to be set up as by `xmltok-forward'." (setq atts nil)) (t (setq atts (cdr atts))))) value-boundary)) - + (defun nxml-compute-indent-in-delimited-token (pos open-delim close-delim) "Return the indent for a line that starts inside a token with delimiters. OPEN-DELIM and CLOSE-DELIM are strings giving the opening and closing @@ -1590,7 +1606,7 @@ of the line. This expects the xmltok-* variables to be set up as by Inserts as many characters as can be completed. However, if not even one character can be completed, then a buffer with the possibilities is popped up and the symbol is read from the minibuffer with -completion. If the symbol is complete, then any characters that must +completion. If the symbol is complete, then any characters that must follow the symbol are also inserted. The name space used for completion and what is treated as a symbol @@ -1612,11 +1628,11 @@ This is the equivalent of `forward-sexp' for XML. An element contains as items strings with no markup, tags, processing instructions, comments, CDATA sections, entity references and -characters references. However, if the variable +characters references. However, if the variable `nxml-sexp-element-flag' is non-nil, then an element is treated as a single markup item. A start-tag contains an element name followed by -one or more attributes. An end-tag contains just an element name. An -attribute value literals contains strings with no markup, entity +one or more attributes. An end-tag contains just an element name. +An attribute value literals contains strings with no markup, entity references and character references. A processing instruction consists of a target and a content string. A comment or a CDATA section contains a single string. An entity reference contains a @@ -2116,7 +2132,7 @@ HAD-DATA says whether there have been non-whitespace data characters yet." (goto-char (+ xmltok-start offset)) (and (re-search-forward "^[ \t]*$" end t) (match-beginning 0))))) - ((and (memq xmltok-type '(start-tag + ((and (memq xmltok-type '(start-tag end-tag empty-element comment @@ -2132,7 +2148,7 @@ HAD-DATA says whether there have been non-whitespace data characters yet." (looking-at "[ \t]*$") (not (nxml-in-mixed-content-p t))) (save-excursion - (or (search-forward "\n" nil t) + (or (search-forward "\n" nil t) (point-max)))))) (defun nxml-paragraph-start-pos (had-data offset) @@ -2172,7 +2188,7 @@ HAD-DATA says whether there have been non-whitespace data characters yet." (goto-char (- (point) offset)) (and (re-search-backward "^[ \t]*$" xmltok-start t) (match-beginning 0)))) - ((and (memq xmltok-type '(start-tag + ((and (memq xmltok-type '(start-tag end-tag empty-element comment @@ -2180,7 +2196,7 @@ HAD-DATA says whether there have been non-whitespace data characters yet." entity-ref)) (nxml-token-ends-line-p) (nxml-token-begins-line-p)) - (or (search-forward "\n" nil t) + (or (search-forward "\n" nil t) (point-max))) ((and (eq xmltok-type 'start-tag) (nxml-token-begins-line-p) @@ -2320,7 +2336,7 @@ ENDP is t in the former case, nil in the latter." (fill-region-as-paragraph start end arg)) (skip-line-prefix fill-prefix) fill-prefix)) - + (defun nxml-newline-and-indent (soft) (delete-horizontal-space) (if soft (insert-and-inherit ?\n) (newline 1)) @@ -2342,10 +2358,10 @@ point. The start-tag will be inserted at or before the beginning of the word before point; the contents of the current buffer is used to decide where. -It works in a similar way to \\[dabbrev-expand]. It searches first +It works in a similar way to \\[dabbrev-expand]. It searches first backwards from point, then forwards from point for an element whose content is a string which matches the contents of the buffer before -point and which includes at least the word before point. It then +point and which includes at least the word before point. It then copies the start- and end-tags from that element and uses them to surround the matching string before point. @@ -2451,7 +2467,7 @@ and attempts to find another possible way to do the markup." (- start-tag-close-pos xmltok-start))) (insert "") (setq nxml-dynamic-markup-prev-pos (point)))))))))) - + ;;; Character names @@ -2480,10 +2496,10 @@ and NAME is a string naming a character.") (defvar nxml-autoload-char-name-set-list nil "List of char namesets that can be autoloaded.") -(defun nxml-enable-char-name-set (nameset) +(defun nxml-enable-char-name-set (nameset) (put nameset 'nxml-char-name-set-enabled t)) -(defun nxml-disable-char-name-set (nameset) +(defun nxml-disable-char-name-set (nameset) (put nameset 'nxml-char-name-set-enabled nil)) (defun nxml-char-name-set-enabled-p (nameset) @@ -2498,9 +2514,9 @@ and NAME is a string naming a character.") (defun nxml-define-char-name-set (nameset alist) "Define a set of character names. NAMESET is a symbol identifying the set. -Alist is a list where each member has the form (NAME CODE), -where NAME is a string naming a character and code -is an integer giving the Unicode scalar value of the character." +ALIST is a list where each member has the form (NAME CODE), +where NAME is a string naming a character and code is an +integer giving the Unicode scalar value of the character." (when (get nameset 'nxml-char-name-set-defined) (error "Nameset `%s' already defined" nameset)) (let ((iter alist)) @@ -2559,7 +2575,7 @@ With a prefix argument, inserts the character directly." (error "Character %x is not supported by Emacs" code)) (format "&#x%X;" code)))))) - + (defun nxml-maybe-load-char-name-set (sym) (when (and (get sym 'nxml-char-name-set-enabled) (not (get sym 'nxml-char-name-set-defined)) @@ -2567,20 +2583,14 @@ With a prefix argument, inserts the character directly." (load (get sym 'nxml-char-name-set-file)))) (defun nxml-toggle-char-ref-extra-display (arg) - "*Toggle the display of extra information for character references." + "Toggle the display of extra information for character references." (interactive "P") (let ((new (if (null arg) (not nxml-char-ref-extra-display) (> (prefix-numeric-value arg) 0)))) (when (not (eq new nxml-char-ref-extra-display)) (setq nxml-char-ref-extra-display new) - (save-excursion - (save-restriction - (widen) - (if nxml-char-ref-extra-display - (nxml-with-unmodifying-text-property-changes - (nxml-clear-fontified (point-min) (point-max))) - (nxml-clear-char-ref-extra-display (point-min) (point-max)))))))) + (font-lock-fontify-buffer)))) (put 'nxml-char-ref 'evaporate t) @@ -2611,7 +2621,7 @@ With a prefix argument, inserts the character directly." (defun nxml-start-delimiter-length (type) (or (get type 'nxml-start-delimiter-length) 0)) - + (put 'cdata-section 'nxml-start-delimiter-length 9) (put 'comment 'nxml-start-delimiter-length 4) (put 'processing-instruction 'nxml-start-delimiter-length 2) @@ -2624,7 +2634,7 @@ With a prefix argument, inserts the character directly." (defun nxml-end-delimiter-length (type) (or (get type 'nxml-end-delimiter-length) 0)) - + (put 'cdata-section 'nxml-end-delimiter-length 3) (put 'comment 'nxml-end-delimiter-length 3) (put 'processing-instruction 'nxml-end-delimiter-length 2) @@ -2643,6 +2653,9 @@ With a prefix argument, inserts the character directly." (put 'entity-ref 'nxml-friendly-name "entity reference") (put 'char-ref 'nxml-friendly-name "character reference") +;;;###autoload +(defalias 'xml-mode 'nxml-mode) + (provide 'nxml-mode) ;; arch-tag: 8603bc5f-1ef9-4021-b223-322fb2ca708e