在 Emacs 上漂亮地打印 XML 文件

我使用 emacs 来编辑 xml 文件(nxml-mode) ,这些文件是由机器生成的,没有任何漂亮的标记格式。

我已经搜索了漂亮的打印整个文件缩进和保存它,但无法找到一个自动的方式。

有办法吗? 或者至少在 linux 上有一些编辑器可以做到这一点。

43577 次浏览

I use nXML mode for editing and Tidy when I want to format and indent XML or HTML. There is also an Emacs interface to Tidy.

Tidy looks like a good mode. Must look at it. Will use it if I really need all the features it offers.

Anyway, this problem was nagging me for about a week and I wasn't searching properly. After posting, I started searching and found one site with an elisp function which does it pretty good. The author also suggests using Tidy.

Thanks for answer Marcel (too bad I don't have enough points to upmod you).

Will post about it soon on my blog. Here is a post about it (with a link to Marcel's site).

  1. Emacs nxml-mode can work on presented format, but you'll have to split the lines.
  2. For longer files that simply isn't worth it. Run this stylesheet (ideally with Saxon which IMHO gets the line indents about right) against longer files to get a nice pretty print. For any elements where you want to retain white space add their names alongside 'programlisting' as in 'programlisting yourElementName'

HTH

If you only need pretty indenting without introducing any new line-breaks, you can apply the indent-region command to the entire buffer with these keystrokes:

C-x h
C-M-\

If you also need to introduce line-breaks, so that opening and closing tags are on separate lines, you could use the following very nice elisp function, written by Benjamin Ferrari. I found it on his blog and hope it's ok for me to reproduce it here:

(defun bf-pretty-print-xml-region (begin end)
"Pretty format XML markup in region. You need to have nxml-mode
http://www.emacswiki.org/cgi-bin/wiki/NxmlMode installed to do
this.  The function inserts linebreaks to separate tags that have
nothing but whitespace between them.  It then indents the markup
by using nxml's indentation rules."
(interactive "r")
(save-excursion
(nxml-mode)
(goto-char begin)
(while (search-forward-regexp "\>[ \\t]*\<" nil t)
(backward-char) (insert "\n") (setq end (1+ end)))
(indent-region begin end))
(message "Ah, much better!"))

This doesn't rely on an external tool like Tidy.

You don't even need to write your own function - sgml-mode (a gnu emacs core module) has a built-in pretty printing function called (sgml-pretty-print ...) which takes region beginning and end arguments.

If you are cutting and pasting xml and you find your terminal is chopping the lines in arbitrary places you can use this pretty printer which fixes broken lines first.

I'm afraid I like Benjamin Ferrari version much better. The internal pretty print always places the end tag in a new line after the value, inserting unwanted CR in the tag values.

Emacs can run arbitrary commands with M-|. If you have xmllint installed:

"M-| xmllint --format -" will format the selected region

"C-u M-| xmllint --format -" will do the same, replacing the region with the output

Thanks to Tim Helmstedt above I made st like this:

(defun nxml-pretty-format ()
(interactive)
(save-excursion
(shell-command-on-region (point-min) (point-max) "xmllint --format -" (buffer-name) t)
(nxml-mode)
(indent-region begin end)))

fast and easy. Many thanks.

here's a few tweaks I made to Benjamin Ferrari's version:

  • the search-forward-regexp didn't specify an end, so it would operate on stuff from beginning of region to end of buffer (instead of end of region)
  • Now increments end properly, as Cheeso noted.
  • it would insert a break between <tag></tag>, which modifies its value. Yes, technically we're modifying values of everything here, but an empty start/end is much more likely to be significant. Now uses two separate, slightly more strict searches to avoid that.

Still has the "doesn't rely on external tidy", etc. However, it does require cl for the incf macro.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; pretty print xml region
(defun pretty-print-xml-region (begin end)
"Pretty format XML markup in region. You need to have nxml-mode
http://www.emacswiki.org/cgi-bin/wiki/NxmlMode installed to do
this.  The function inserts linebreaks to separate tags that have
nothing but whitespace between them.  It then indents the markup
by using nxml's indentation rules."
(interactive "r")
(save-excursion
(nxml-mode)
(goto-char begin)
;; split <foo><foo> or </foo><foo>, but not <foo></foo>
(while (search-forward-regexp ">[ \t]*<[^/]" end t)
(backward-char 2) (insert "\n") (incf end))
;; split <foo/></foo> and </foo></foo>
(goto-char begin)
(while (search-forward-regexp "<.*?/.*?>[ \t]*<" end t)
(backward-char) (insert "\n") (incf end))
(indent-region begin end nil)
(normal-mode))
(message "All indented!"))

One way of doing is If you have something in below format

<abc>     <abc><abc>   <abc></abc> </abc></abc>       </abc>

In Emacs, try

M-x nxml-mode
M-x replace-regexp RET  > *< RET >C-q C-j< RET
C-M-\ to indent

This will indent above xml example to below

<abc>
<abc>
<abc>
<abc>
</abc>
</abc>
</abc>
</abc>

In VIM you can do this by

:set ft=xml
:%s/>\s*</>\r</g
ggVG=

Hope this helps.

I took Jason Viers' version and added logic to put xmlns declarations on their own lines. This assumes that you have xmlns= and xmlns: with no intervening whitespace.

(defun cheeso-pretty-print-xml-region (begin end)
"Pretty format XML markup in region. You need to have nxml-mode
http://www.emacswiki.org/cgi-bin/wiki/NxmlMode installed to do
this.  The function inserts linebreaks to separate tags that have
nothing but whitespace between them.  It then indents the markup
by using nxml's indentation rules."
(interactive "r")
(save-excursion
(nxml-mode)
;; split <foo><bar> or </foo><bar>, but not <foo></foo>
(goto-char begin)
(while (search-forward-regexp ">[ \t]*<[^/]" end t)
(backward-char 2) (insert "\n") (incf end))
;; split <foo/></foo> and </foo></foo>
(goto-char begin)
(while (search-forward-regexp "<.*?/.*?>[ \t]*<" end t)
(backward-char) (insert "\n") (incf end))
;; put xml namespace decls on newline
(goto-char begin)
(while (search-forward-regexp "\\(<\\([a-zA-Z][-:A-Za-z0-9]*\\)\\|['\"]\\) \\(xmlns[=:]\\)" end t)
(goto-char (match-end 0))
(backward-char 6) (insert "\n") (incf end))
(indent-region begin end nil)
(normal-mode))
(message "All indented!"))

I use xml-reformat-tags from xml-parse.el. Usually you will want to have the point at the beginning of the file when running this command.

It's interesting that the file is incorporated into Emacspeak. When I was using Emacspeak on day-by-day basis, I thought xml-reformat-tags is an Emacs builtin. One day I lost it and had to make an internet search for that, and thus entered the wiki page mentioned above.

I'm attaching also my code to start xml-parse. Not sure if this is the best piece of Emacs code, but seems to work for me.

(if (file-exists-p "~/.emacs.d/packages/xml-parse.el")
(let ((load-path load-path))
(add-to-list 'load-path "~/.emacs.d/packages")
(require 'xml-parse))
)

For introducing line breaks and then pretty printing

M-x sgml-mode
M-x sgml-pretty-print

If you use spacemacs, just use command 'spacemacs/indent-region-or-buffer'.

M-x spacemacs/indent-region-or-buffer

as of 2017 emacs already comes with this capability by default, but you have to write this little function into your ~/.emacs.d/init.el:

(require 'sgml-mode)


(defun reformat-xml ()
(interactive)
(save-excursion
(sgml-pretty-print (point-min) (point-max))
(indent-region (point-min) (point-max))))

then just call M-x reformat-xml

source: https://davidcapello.com/blog/emacs/reformat-xml-on-emacs/