diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a9e17..43aada7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `<>, <>`, 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) diff --git a/adoc-mode.el b/adoc-mode.el index 5adb7f9..d591456 100644 --- a/adoc-mode.el +++ b/adoc-mode.el @@ -1425,13 +1425,13 @@ further tests to find a proper xref." ((eq type 'inline-special-with-caption) ;; (?su)[\\]?<<(?P[\w"].*?)>>=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) @@ -1501,7 +1501,10 @@ 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))) @@ -1509,7 +1512,7 @@ NOT-ALLOWED-CHARS are chars not allowed before the quote." (adoc-re-quote-precondition "") "\\(\\[[^][]+?\\]\\)?" "\\(" qldel "\\)" - "\\(" (adoc-re-content "+") "\\)" + "\\(" (or content (adoc-re-content "+")) "\\)" "\\(" qrdel "\\)"))) ;; AsciiDoc src for constrained quotes @@ -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")))) @@ -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 @@ -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) @@ -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) ; : @@ -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 "\\)"))) @@ -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 @@ -2978,8 +2992,8 @@ 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 @@ -2987,30 +3001,6 @@ between matching delimiters, never in the surrounding prose." ;; 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") ;; ™ - ;; (^-- )=—  - ;; (\n-- )|( -- )|( --\n)= —  - ;; (\w)--(\w)=\1—\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 - ;; (?> + ;; 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") ;; ™ + ;; (^-- )=—  + ;; (\n-- )|( -- )|( --\n)= —  + ;; (\w)--(\w)=\1—\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 + ;; (?>, <> and <> and <>." + ("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 <> 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" + ("<>" + ("Bob" adoc-reference-face) + ("s page" adoc-reference-face))) + (when-fontifying-it "fontifies the xref macro" ("xref:foo[]" ("xref" adoc-command-face) @@ -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 ---------------------------------------------- @@ -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