Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
- A block now ends at the first line that repeats its opening delimiter exactly, as in Asciidoctor. It used to need a non-blank last line, and any longer run of the same character closed it.
- A delimiter line inside a listing or literal block is just content.
- [#90](https://github.com/bbatsov/adoc-mode/pull/90): Example, open, quote and sidebar blocks keep the highlighting of what's inside them (list markers, comments, `include::` lines, nested code), and section titles inside a delimited block are no longer highlighted as titles, since Asciidoctor reads them as plain text.
- [#66](https://github.com/bbatsov/adoc-mode/issues/66): Links, `xref:` and footnote macros and the like are recognised (and clickable) again when their text holds an apostrophe, an ellipsis, an arrow or other inline markup, as in `https://example.org[Bob's page]`. Bare URLs no longer run into a following `[`.
- [#91](https://github.com/bbatsov/adoc-mode/pull/91): Two cross-references on one line, as in `<<foo>>, <<bar>>`, are highlighted as two instead of one with the id `foo>>`, and an xref to a section's auto-id like `<<_installation>>` is highlighted at all.
- [#91](https://github.com/bbatsov/adoc-mode/pull/91): Superscripts and subscripts can't span whitespace anymore, as in Asciidoctor, so `~/.emacs.d/init.el to ~/backup` and `C-^ to join and M-^` stay plain text.
- [#91](https://github.com/bbatsov/adoc-mode/pull/91): Superscripts are raised by their own `adoc-script-raise` value. They checked the subscript's, so setting that to 0 stopped superscripts from being raised.

## 0.9.0 (2026-06-02)

Expand Down
101 changes: 59 additions & 42 deletions adoc-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -1425,13 +1425,13 @@ further tests to find a proper xref."
((eq type 'inline-special-with-caption)
;; (?su)[\\]?&lt;&lt;(?P<attrlist>[\w"].*?)&gt;&gt;=xref2
(if for-kw
"\\(<<\\)\\([a-zA-Z0-9\"].*?\\)\\(,\\)\\(.*?\\(?:\n.*?\\)??\\)\\(>>\\)"
"\\(<<\\)\\([[:alnum:]_#/.:{\"][^,>\n]*?\\(?:\n[^,>\n]*?\\)??\\)\\(,\\)\\(.*?\\(?:\n.*?\\)??\\)\\(>>\\)"
(concat "\\(<<\\)\\(" (adoc-re-id) "[ \t\n]*\\)\\(,\\)\\([^>\n]*?\\(?:\n[^>\n]*?\\)??\\)\\(>>\\)")))

((eq type 'inline-special-no-caption)
;; asciidoc.conf uses the same regexp as for without caption
(if for-kw
"\\(<<\\)\\([a-zA-Z0-9\"].*?\\(?:\n.*?\\)??\\)\\(>>\\)"
"\\(<<\\)\\([[:alnum:]_#/.:{\"][^,>\n]*?\\(?:\n[^,>\n]*?\\)??\\)\\(>>\\)"
(concat "\\(<<\\)\\(" (adoc-re-id) "[ \t\n]*\\)\\(>>\\)")))

((eq type 'inline-general-macro)
Expand Down Expand Up @@ -1501,15 +1501,18 @@ NOT-ALLOWED-CHARS are chars not allowed before the quote."
;; backslash as the first char. If the first char is ineed a backslash, it is
;; 'removed' (-> adoc-meta-hide-face face), and the rest of the match is left
;; unaffected.
(defun adoc-re-unconstrained-quote (ldel &optional rdel)
(defun adoc-re-unconstrained-quote (ldel &optional rdel content)
"Return a regexp matching an unconstrained quote between LDEL and RDEL.
RDEL defaults to LDEL. CONTENT is the regexp for the quoted text,
by default any text spanning up to a paragraph."
(unless rdel (setq rdel ldel))
(let* ((qldel (regexp-quote ldel))
(qrdel (regexp-quote rdel)))
(concat
(adoc-re-quote-precondition "")
"\\(\\[[^][]+?\\]\\)?"
"\\(" qldel "\\)"
"\\(" (adoc-re-content "+") "\\)"
"\\(" (or content (adoc-re-content "+")) "\\)"
"\\(" qrdel "\\)")))

;; AsciiDoc src for constrained quotes
Expand Down Expand Up @@ -1549,6 +1552,10 @@ subgroups:
(adoc-re-constrained-quote ldel rdel))
((eq type 'adoc-unconstrained)
(adoc-re-unconstrained-quote ldel rdel))
((eq type 'adoc-script)
;; Super- and subscripts can't contain whitespace (`\S+?' in
;; Asciidoctor), so `~/.emacs.d/init.el to ~/backup' isn't one.
(adoc-re-unconstrained-quote ldel rdel "[^ \t\n]+?"))
(t
(error "Invalid type"))))

Expand Down Expand Up @@ -1788,17 +1795,19 @@ nil, so the search goes on to the next match instead."
(goto-char attribute-list-end))))
nil)

(defun adoc-facespec-subscript ()
(defun adoc-facespec-script (face raise)
"Return a quoted face spec for a super- or subscript in FACE.
The text is displayed RAISE lines higher, unless RAISE is 0."
(list 'quote
(append '(face adoc-subscript-face)
(when (not (= 0 (car adoc-script-raise)))
`(display (raise ,(car adoc-script-raise)))))))
(append `(face ,face)
(unless (zerop raise)
`(display (raise ,raise))))))

(defun adoc-facespec-subscript ()
(adoc-facespec-script 'adoc-subscript-face (car adoc-script-raise)))

(defun adoc-facespec-superscript ()
(list 'quote
(append '(face adoc-superscript-face)
(when (not (= 0 (car adoc-script-raise)))
`(display (raise ,(cadr adoc-script-raise)))))))
(adoc-facespec-script 'adoc-superscript-face (cadr adoc-script-raise)))

;; TODO: use & learn some more macro magic so adoc-kw-unconstrained-quote and
;; adoc-kw-constrained-quote are less redundant and have common parts in one
Expand Down Expand Up @@ -2112,7 +2121,7 @@ TEXTPROPS is an additional plist with textproperties."
',(car target-faces)
',(cadr target-faces)))
(t (list 'quote target-faces)))
'adoc-flyspell-ignore t)
'adoc-flyspell-ignore t 'adoc-reserved t)
,(if target-meta-p t 'append))
`(4 '(face adoc-meta-face adoc-reserved t . ,textprops) t) ; [
`(5 '(face adoc-meta-face adoc-attribute-list ,(or attribute-list t) . ,textprops) t)
Expand All @@ -2123,7 +2132,9 @@ TEXTPROPS is an additional plist with textproperties."
(defun adoc-kw-inline-macro-urls-attribute-list ()
(let ((cmd-name (regexp-opt '("http" "https" "ftp" "file" "irc" "mailto" "callto" "link"))))
(list
`(lambda (end) (adoc-kwf-std end ,(adoc-re-inline-macro cmd-name) '(0) '(0)))
;; The link text (group 5) may hold other, already highlighted, markup,
;; e.g. `https://example.org[the *bold* text]'.
`(lambda (end) (adoc-kwf-std end ,(adoc-re-inline-macro cmd-name) '(1 2 3 4 6) '(0)))
'(0 '(face nil keymap adoc-link-keymap mouse-face adoc-link-mouse-face help-echo "mouse-1: visit this link")) ; clickable
`(1 '(face adoc-url-face adoc-reserved t adoc-flyspell-ignore t) t) ; cmd-name
`(2 '(face adoc-url-face adoc-reserved t) t) ; :
Expand Down Expand Up @@ -2174,7 +2185,7 @@ TEXTPROPS is an additional plist with textproperties."
;; because part of the match (the __) contains text properties with
;; adoc-reserved non-nil, also because quote highlighting already happened.
(defun adoc-kw-standalone-urls ()
(let* ((url "\\b\\(?:https?\\|ftp\\|file\\|irc\\)://[^ \t\n<>]*[a-zA-Z0-9_/]")
(let* ((url "\\b\\(?:https?\\|ftp\\|file\\|irc\\)://[^][ \t\n<>]*[a-zA-Z0-9_/]")
(url<> (concat "<\\(?:" url "\\)>"))
(email "[a-zA-Z0-9_][-a-zA-Z0-9_._]*@[-a-zA-Z0-9_._]*[a-zA-Z0-9_]")
(both (concat "\\(?:" url "\\)\\|\\(?:" url<> "\\)\\|\\(?:" email "\\)")))
Expand All @@ -2193,8 +2204,11 @@ TEXTPROPS is an additional plist with textproperties."
;; matcher function
(lambda (end)
(let (found)
;; Skip reserved text, and attribute lists (link text and the like),
;; whose face is applied to the list as a whole later on.
(while (and (setq found (adoc-kwf-search regexp end t))
(text-property-not-all (match-beginning 1) (match-end 1) 'adoc-reserved nil))
(or (text-property-not-all (match-beginning 1) (match-end 1) 'adoc-reserved nil)
(text-property-not-all (match-beginning 1) (match-end 1) 'adoc-attribute-list nil)))
(goto-char (+ (match-beginning 0) 1)))
(when (and found adoc-insert-replacement replacement)
(let* ((s (cond
Expand Down Expand Up @@ -2978,39 +2992,15 @@ between matching delimiters, never in the surrounding prose."
(adoc-kw-quote 'adoc-constrained "_" 'adoc-emphasis-face)
(adoc-kw-quote 'adoc-unconstrained "##" 'adoc-highlight-face) ; highlighted text
(adoc-kw-quote 'adoc-constrained "#" 'adoc-highlight-face) ; highlighted text
(adoc-kw-quote 'adoc-unconstrained "~" (adoc-facespec-subscript)) ; subscript
(adoc-kw-quote 'adoc-unconstrained "^" (adoc-facespec-superscript)) ; superscript
(adoc-kw-quote 'adoc-script "~" (adoc-facespec-subscript)) ; subscript
(adoc-kw-quote 'adoc-script "^" (adoc-facespec-superscript)) ; superscript


;; special words
;; --------------------
;; there are no default special words to highlight


;; replacements
;; --------------------------------
;; Asciidoc.conf surrounds em dash with thin spaces. I think that does not
;; make sense here, all that spaces you would see in the buffer would at best
;; be confusing.
(adoc-kw-replacement "\\((C)\\)" "\u00A9") ;; ©
(adoc-kw-replacement "\\((R)\\)" "\u00AE") ;; ®
(adoc-kw-replacement "\\((TM)\\)" "\u2122") ;; ™
;; (^-- )=&#8212;&#8201;
;; (\n-- )|( -- )|( --\n)=&#8201;&#8212;&#8201;
;; (\w)--(\w)=\1&#8212;\2
(adoc-kw-replacement "^\\(--\\)[ \t]" "\u2014") ; em dash. See also above
(adoc-kw-replacement "[ \t]\\(--\\)\\(?:[ \t]\\|$\\)" "\u2014") ; dito
(adoc-kw-replacement "[a-zA-Z0-9_]\\(--\\)[a-zA-Z0-9_]" "\u2014") ; dito
(adoc-kw-replacement "[a-zA-Z0-9_]\\('\\)[a-zA-Z0-9_]" "\u2019") ; punctuation apostrophe
(adoc-kw-replacement "\\(\\.\\.\\.\\)" "\u2026") ; ellipsis
(adoc-kw-replacement "\\(->\\)" "\u2192")
(adoc-kw-replacement "\\(=>\\)" "\u21D2")
(adoc-kw-replacement "\\(<-\\)" "\u2190")
(adoc-kw-replacement "\\(<=\\)" "\u21D0")
;; general character entity reference
;; (?<!\\)&amp;([:_#a-zA-Z][:_.\-\w]*?;)=&\1
(adoc-kw-replacement "\\(&[:_#a-zA-Z]\\(?:[-:_.]\\|[a-zA-Z0-9_]\\)*?;\\)" 'adoc-entity-to-string)

;; attributes
;; ---------------------------------
;; attribute reference, including the `:'-separated reference macros
Expand Down Expand Up @@ -3097,6 +3087,33 @@ between matching delimiters, never in the surrounding prose."
'(2 'adoc-reference-face) ; link text = anchor id
'(3 'adoc-meta-hide-face)) ; >>

;; replacements
;; --------------------------------
;; They come after the inline macros and cross references, which may
;; contain them (`https://example.org[Bob's page]'): a replacement reserves
;; its text, and a macro running later would refuse to match across it.
;; Asciidoc.conf surrounds em dash with thin spaces. I think that does not
;; make sense here, all that spaces you would see in the buffer would at best
;; be confusing.
(adoc-kw-replacement "\\((C)\\)" "\u00A9") ;; ©
(adoc-kw-replacement "\\((R)\\)" "\u00AE") ;; ®
(adoc-kw-replacement "\\((TM)\\)" "\u2122") ;; ™
;; (^-- )=&#8212;&#8201;
;; (\n-- )|( -- )|( --\n)=&#8201;&#8212;&#8201;
;; (\w)--(\w)=\1&#8212;\2
(adoc-kw-replacement "^\\(--\\)[ \t]" "\u2014") ; em dash. See also above
(adoc-kw-replacement "[ \t]\\(--\\)\\(?:[ \t]\\|$\\)" "\u2014") ; dito
(adoc-kw-replacement "[a-zA-Z0-9_]\\(--\\)[a-zA-Z0-9_]" "\u2014") ; dito
(adoc-kw-replacement "[a-zA-Z0-9_]\\('\\)[a-zA-Z0-9_]" "\u2019") ; punctuation apostrophe
(adoc-kw-replacement "\\(\\.\\.\\.\\)" "\u2026") ; ellipsis
(adoc-kw-replacement "\\(->\\)" "\u2192")
(adoc-kw-replacement "\\(=>\\)" "\u21D2")
(adoc-kw-replacement "\\(<-\\)" "\u2190")
(adoc-kw-replacement "\\(<=\\)" "\u21D0")
;; general character entity reference
;; (?<!\\)&amp;([:_#a-zA-Z][:_.\-\w]*?;)=&\1
(adoc-kw-replacement "\\(&[:_#a-zA-Z]\\(?:[-:_.]\\|[a-zA-Z0-9_]\\)*?;\\)" 'adoc-entity-to-string)

;; index terms
;; TODO:
;; - copy asciidocs regexps below
Expand Down
108 changes: 106 additions & 2 deletions test/adoc-mode-font-lock-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,31 @@
("a ^super^ word"
("super" adoc-superscript-face))
("a ~sub~ word"
("sub" adoc-subscript-face))))
("sub" adoc-subscript-face)))

(when-fontifying-it "doesn't let superscript and subscript span whitespace"
("Copy ~/.emacs.d/init.el to ~/backup, then H~2~O."
("to" nil)
("2" adoc-subscript-face))
("Press C-^ to join and M-^ to split, E=mc^2^."
("to join and M-" nil)
("2" adoc-superscript-face)))

(it "raises superscript by its own `adoc-script-raise' value"
(let ((adoc-script-raise '(0 0.3)))
(unwind-protect
(progn
(adoc-calc)
(with-temp-buffer
(adoc-mode)
(insert "E=mc^2^ and H~2~O")
(font-lock-ensure)
(goto-char (point-min))
(search-forward "^2")
(expect (get-text-property (1- (point)) 'display) :to-equal '(raise 0.3))
(search-forward "~2")
(expect (get-text-property (1- (point)) 'display) :to-be nil)))
(adoc-calc)))))

;; ---- Passthroughs --------------------------------------------------

Expand Down Expand Up @@ -583,6 +607,29 @@
("sect-id" adoc-meta-face)
("the caption" adoc-reference-face)))

(when-fontifying-it "keeps xrefs on the same line apart"
("See <<foo>>, <<bar>> and <<a>> and <<b,text>>."
("foo" adoc-reference-face)
(", " nil)
("bar" adoc-reference-face)
("a" adoc-reference-face)
("b" adoc-meta-face)
("text" adoc-reference-face)))

(when-fontifying-it "fontifies a captioned xref whose id wraps"
("See <<Installing the\nSoftware,the install guide>> now."
("Installing" adoc-meta-face)
("the install guide" adoc-reference-face)))

(when-fontifying-it "fontifies an xref to a section's auto-id"
("see <<_installation>> end"
("installation" adoc-reference-face)))

(when-fontifying-it "fontifies an xref caption with an apostrophe"
("<<id,Bob's page>>"
("Bob" adoc-reference-face)
("s page" adoc-reference-face)))

(when-fontifying-it "fontifies the xref macro"
("xref:foo[]"
("xref" adoc-command-face)
Expand Down Expand Up @@ -614,7 +661,50 @@

(when-fontifying-it "fontifies a bare URL"
("see http://www.lorem.com/x.html here"
("http://www.lorem.com/x.html" adoc-url-face))))
("http://www.lorem.com/x.html" adoc-url-face)))

(when-fontifying-it "fontifies links whose text has replacements in it"
;; an apostrophe, an ellipsis or an arrow used to stop the link
;; from being recognised at all
("https://example.org/a[Bob's page]"
("https://example.org/a" adoc-url-face)
("Bob's page" adoc-reference-face))
("https://example.org[A -> B...]"
("A -> B..." adoc-reference-face))
("see footnote:[it's here] and xref:a.adoc[Bob's]"
("it's here" adoc-footnote-text-face)
("Bob's" adoc-reference-face)))

(it "fontifies links whose text has other markup in it"
(dolist (text '("https://example.org[the *bold* text]"
"https://example.org[the `code` text]"))
(with-temp-buffer
(insert text)
(adoc-mode)
(font-lock-ensure)
(goto-char (point-min))
(search-forward "the")
(expect (get-text-property (point) 'keymap) :to-be 'adoc-link-keymap)
(expect (get-text-property (match-beginning 0) 'face)
:to-equal 'adoc-reference-face))))

(when-fontifying-it "fontifies a URL with a double dash in it"
("https://example.org/a--b[link]"
("https://example.org/a--b" adoc-url-face)))

(when-fontifying-it "ends a bare URL at a bracket"
("see https://example.org[oops"
("https://example.org" adoc-url-face)
("oops" nil)))

(it "makes a link with an apostrophe in its text clickable"
(with-temp-buffer
(insert "https://example.org/a[Bob's page]")
(adoc-mode)
(font-lock-ensure)
(goto-char (point-min))
(search-forward "Bob")
(expect (get-text-property (point) 'keymap) :to-be 'adoc-link-keymap))))

;; ---- Role-based spans ----------------------------------------------

Expand Down Expand Up @@ -732,6 +822,20 @@
(overlays-in (point-min) (point-max)))))
(expect ov :not :to-be nil)
(expect (overlay-get ov 'after-string) :to-equal (cdr case))))))
(adoc-calc))))

(it "doesn't put replacement overlays in link text or URLs"
(let ((adoc-insert-replacement t))
(unwind-protect
(progn
(adoc-calc)
(with-temp-buffer
(adoc-mode)
(insert "https://example.org/a--b[Bob's page]")
(font-lock-ensure)
(expect (seq-filter (lambda (o) (overlay-get o 'adoc-kw-replacement))
(overlays-in (point-min) (point-max)))
:to-be nil)))
(adoc-calc))))))

;;; adoc-mode-font-lock-test.el ends here
Loading