Browse Source

Change bracket link escape syntax

* contrib/lisp/org-link-edit.el (org-link-edit--link-data):
* lisp/ob-tangle.el (org-babel-tangle-comment-links): Update match-group.
(org-babel-detangle): Remove unnecessary `org-link-escape' call.
(org-babel-tangle-jump-to-org): Update match group.
(org-link-url-hexify):
(org-link-escape-chars): Remove variables.
* lisp/ol.el (org-link--decode-compound): Renamed from
  `org-link--unescape-compound'.
(org-link--decode-single-byte-sequence): Renamed from
`org-link--unescape-single-byte-sequence'.
(org-link-make-regexps): Update `org-link-bracket-re' syntax.
(org-link-encode): New function, renamed from `org-link-escape'.
(org-link-decode): New function, renamed from `org-link-unescape'.
(org-link-escape):
(org-link-unescape): Use new escape syntax.
(org-link-make-string): Apply new escaping rules.
(org-link-display-format):
(org-insert-link): Update match group.
* lisp/org-agenda.el (org-diary):
(org-agenda-format-item):
(org-agenda-to-appt): Update match group.
* lisp/org-clock.el (org-clocktable-write-default): Update match group.
* lisp/org-element.el (org-element-link-parser): Update match group.
* lisp/org-mobile.el (org-mobile-escape-olp):
(org-mobile-locate-entry): Apply function renaming.
* lisp/org-protocol.el (org-protocol-split-data):
(org-protocol-parse-parameters): Apply function renaming.
* lisp/org.el (org-refile): Update match group.
* testing/README (Interactive testing from within Emacs): Fix
  examples.
* testing/lisp/test-ol.el (test-ol/encode): Merge old escape tests.
(test-ol/decode): Merge old unescape tests.
(test-ol/escape):
(test-ol/unescape):
(test-ol/make-string): New tests.
* testing/lisp/test-org-clock.el (test-org-clock/clocktable/link):
* testing/lisp/test-org.el (test-org/custom-id):
(test-org/fuzzy-links):
* testing/lisp/test-ox.el (test-org-export/resolve-fuzzy-link): Update
  tests.
Nicolas Goaziou 4 months ago
parent
commit
2b00d62816

+ 1 - 3
contrib/lisp/org-link-edit.el

@@ -95,9 +95,7 @@ The list includes
               (match-end 0)
               (save-match-data
                 (org-link-unescape (match-string-no-properties 1)))
-              (or (and (match-end 3)
-                       (match-string-no-properties 3))
-                  "")))
+              (or (match-string-no-properties 2) "")))
        ((looking-at org-plain-link-re)
         (list (match-beginning 0)
               (match-end 0)

+ 4 - 14
lisp/ob-tangle.el

@@ -30,6 +30,7 @@
 (require 'cl-lib)
 (require 'org-src)
 (require 'org-macs)
+(require 'ol)
 
 (declare-function make-directory "files" (dir &optional parents))
 (declare-function org-at-heading-p "org" (&optional ignored))
@@ -40,14 +41,8 @@
 (declare-function org-element-type "org-element" (element))
 (declare-function org-heading-components "org" ())
 (declare-function org-in-commented-heading-p "org" (&optional no-inheritance))
-(declare-function org-link-escape "org" (text &optional table merge))
-(declare-function org-link-open-from-string "ol" (s &optional arg))
-(declare-function org-link-trim-scheme "ol" (uri))
-(declare-function org-store-link "org" (arg &optional interactive?))
 (declare-function outline-previous-heading "outline" ())
 
-(defvar org-link-types-re)
-
 (defcustom org-babel-tangle-lang-exts
   '(("emacs-lisp" . "el")
     ("elisp" . "el"))
@@ -330,8 +325,6 @@ references."
     (delete-region (save-excursion (beginning-of-line 1) (point))
                    (save-excursion (end-of-line 1) (forward-char 1) (point)))))
 
-(defvar org-stored-links)
-(defvar org-link-bracket-re)
 (defun org-babel-spec-to-string (spec)
   "Insert SPEC into the current file.
 
@@ -506,10 +499,7 @@ non-nil, return the full association list to be used by
 	 `(("start-line" . ,(number-to-string
 			     (org-babel-where-is-src-block-head)))
 	   ("file" . ,(buffer-file-name))
-	   ("link" . ,(org-link-escape
-		       (progn
-			 (call-interactively #'org-store-link)
-			 (org-no-properties (car (pop org-stored-links))))))
+	   ("link" . ,(org-no-properties (org-store-link nil)))
 	   ("source-name" .
 	    ,(nth 4 (or info (org-babel-get-src-block-info 'light)))))))
     (list (org-fill-template org-babel-tangle-comment-format-beg link-data)
@@ -527,7 +517,7 @@ which enable the original code blocks to be found."
     (let ((counter 0) new-body end)
       (while (re-search-forward org-link-bracket-re nil t)
         (when (re-search-forward
-	       (concat " " (regexp-quote (match-string 3)) " ends here"))
+	       (concat " " (regexp-quote (match-string 2)) " ends here"))
           (setq end (match-end 0))
           (forward-line -1)
           (save-excursion
@@ -549,7 +539,7 @@ which enable the original code blocks to be found."
 		     (and (setq start (line-beginning-position))
 			  (setq body-start (line-beginning-position 2))
 			  (setq link (match-string 0))
-			  (setq block-name (match-string 3))
+			  (setq block-name (match-string 2))
 			  (save-excursion
 			    (save-match-data
 			      (re-search-forward

+ 81 - 66
lisp/ol.el

@@ -375,13 +375,6 @@ single keystroke rather than having to type \"yes\"."
   :tag "Org Store Link"
   :group 'org-link)
 
-(defcustom org-link-url-hexify t
-  "When non-nil, hexify URL when creating a link."
-  :type 'boolean
-  :version "24.3"
-  :group 'org-link-store
-  :safe #'booleanp)
-
 (defcustom org-link-context-for-files t
   "Non-nil means file links from `org-store-link' contain context.
 \\<org-mode-map>
@@ -451,12 +444,6 @@ links more efficient."
 
 ;;; Public variables
 
-(defconst org-link-escape-chars
-  ;;%20 %5B %5D %25
-  '(?\s ?\[ ?\] ?%)
-  "List of characters that should be escaped in a link when stored to Org.
-This is the list that is used for internal purposes.")
-
 (defconst org-target-regexp (let ((border "[^<>\n\r \t]"))
 			      (format "<<\\(%s\\|%s[^<>\n\r]*%s\\)>>"
 				      border border border))
@@ -597,7 +584,7 @@ either a link description or nil."
     (concat (format "%-45s" (substring desc 0 (min (length desc) 40)))
 	    "<" (car link) ">")))
 
-(defun org-link--unescape-compound (hex)
+(defun org-link--decode-compound (hex)
   "Unhexify Unicode hex-chars HEX.
 E.g. \"%C3%B6\" is the German o-Umlaut.  Note: this function also
 decodes single byte encodings like \"%E1\" (a-acute) if not
@@ -628,14 +615,15 @@ followed by another \"%[A-F0-9]{2}\" group."
 	    (setq ret (concat ret (char-to-string sum)))
 	    (setq sum 0))
 	   ((not bytes)			; single byte(s)
-	    (setq ret (org-link--unescape-single-byte-sequence hex))))))
+	    (setq ret (org-link--decode-single-byte-sequence hex))))))
       ret)))
 
-(defun org-link--unescape-single-byte-sequence (hex)
+(defun org-link--decode-single-byte-sequence (hex)
   "Unhexify hex-encoded single byte character sequence HEX."
   (mapconcat (lambda (byte)
 	       (char-to-string (string-to-number byte 16)))
-	     (cdr (split-string hex "%")) ""))
+	     (cdr (split-string hex "%"))
+	     ""))
 
 (defun org-link--fontify-links-to-this-file ()
   "Fontify links to the current file in `org-stored-links'."
@@ -750,7 +738,18 @@ This should be called after the variable `org-link-parameters' has changed."
 	   "\\([^][ \t\n()<>]+\\(?:([[:word:]0-9_]+)\\|\\([^[:punct:] \t\n]\\|/\\)\\)\\)")
 	  ;;	 "\\([^]\t\n\r<>() ]+[^]\t\n\r<>,.;() ]\\)")
 	  org-link-bracket-re
-	  "\\[\\[\\([^][]+\\)\\]\\(\\[\\([^][]+\\)\\]\\)?\\]"
+	  (rx (seq "[["
+		   ;; URI part: match group 1.
+		   (group
+		    (*? anything)
+		    ;; Allow an even number of backslashes right
+		    ;; before the closing bracket.
+		    (not (any "\\"))
+		    (zero-or-more "\\\\"))
+		   "]"
+		   ;; Description (optional): match group 2.
+		   (opt "[" (group (+? anything)) "]")
+		   "]"))
 	  org-link-any-re
 	  (concat "\\(" org-link-bracket-re "\\)\\|\\("
 		  org-link-angle-re "\\)\\|\\("
@@ -841,55 +840,71 @@ and dates."
       (setq org-store-link-plist
 	    (plist-put org-store-link-plist key value)))))
 
-(defun org-link-escape (text &optional table merge)
-  "Return percent escaped representation of TEXT.
-TEXT is a string with the text to escape.
-Optional argument TABLE is a list with characters that should be
-escaped.  When nil, `org-link-escape-chars' is used.
-If optional argument MERGE is set, merge TABLE into
-`org-link-escape-chars'."
-  (let ((characters-to-encode
-	 (cond ((null table) org-link-escape-chars)
-	       (merge (append org-link-escape-chars table))
-	       (t table))))
-    (mapconcat
-     (lambda (c)
-       (if (or (memq c characters-to-encode)
-	       (and org-link-url-hexify (or (< c 32) (> c 126))))
-	   (mapconcat (lambda (e) (format "%%%.2X" e))
-		      (or (encode-coding-char c 'utf-8)
-			  (error "Unable to percent escape character: %c" c))
-		      "")
-	 (char-to-string c)))
-     text "")))
-
-(defun org-link-unescape (str)
-  "Unhex hexified Unicode parts in string STR.
-E.g. \"%C3%B6\" becomes the german o-Umlaut.  This is the
-reciprocal of `org-link-escape', which see."
-  (if (org-string-nw-p str)
-      (replace-regexp-in-string
-       "\\(%[0-9A-Za-z]\\{2\\}\\)+" #'org-link--unescape-compound str t t)
-    str))
+(defun org-link-encode (text table)
+  "Return percent escaped representation of string TEXT.
+TEXT is a string with the text to escape. TABLE is a list of
+characters that should be escaped."
+  (mapconcat
+   (lambda (c)
+     (if (memq c table)
+	 (mapconcat (lambda (e) (format "%%%.2X" e))
+		    (or (encode-coding-char c 'utf-8)
+			(error "Unable to percent escape character: %c" c))
+		    "")
+       (char-to-string c)))
+   text ""))
+
+(defun org-link-decode (s)
+  "Decode percent-encoded parts in string S.
+E.g. \"%C3%B6\" becomes the german o-Umlaut."
+  (replace-regexp-in-string "\\(%[0-9A-Za-z]\\{2\\}\\)+"
+			    #'org-link--decode-compound s t t))
+
+(defun org-link-escape (link)
+  "Backslash-escape sensitive characters in string LINK."
+  ;; Escape closing square brackets followed by another square bracket
+  ;; or at the end of the link.  Also escape final backslashes so that
+  ;; we do not escape inadvertently URI's closing bracket.
+  (with-temp-buffer
+    (insert link)
+    (insert (make-string (- (skip-chars-backward "\\\\"))
+			 ?\\))
+    (while (search-backward "\]" nil t)
+      (when (looking-at-p "\\]\\(?:[][]\\|\\'\\)")
+	(insert (make-string (1+ (- (skip-chars-backward "\\\\")))
+			     ?\\))))
+    (buffer-string)))
+
+(defun org-link-unescape (link)
+  "Remove escaping backslash characters from string LINK."
+  (with-temp-buffer
+    (save-excursion (insert link))
+    (while (re-search-forward "\\(\\\\+\\)\\]\\(?:[][]\\|\\'\\)" nil t)
+      (replace-match (make-string (/ (- (match-end 1) (match-beginning 1)) 2)
+				  ?\\)
+		     nil t nil 1))
+    (goto-char (point-max))
+    (delete-char (/ (- (skip-chars-backward "\\\\")) 2))
+    (buffer-string)))
 
 (defun org-link-make-string (link &optional description)
-  "Make a bracket link, consisting of LINK and DESCRIPTION."
+  "Make a bracket link, consisting of LINK and DESCRIPTION.
+LINK is escaped with backslashes for inclusion in buffer."
   (unless (org-string-nw-p link) (error "Empty link"))
-  (let ((uri (cond ((string-match org-link-types-re link)
-		    (concat (match-string 1 link)
-			    (org-link-escape (substring link (match-end 1)))))
-		   ((or (file-name-absolute-p link)
-			(string-match-p "\\`\\.\\.?/" link))
-		    (org-link-escape link))
-		   ;; For readability, do not encode space characters
-		   ;; in fuzzy links.
-		   (t (org-link-escape link (remq ?\s org-link-escape-chars)))))
-	(description
-	 (and (org-string-nw-p description)
-	      ;; Remove brackets from description, as they are fatal.
-	      (replace-regexp-in-string
-	       "[][]" (lambda (m) (if (equal "[" m) "{" "}"))
-	       (org-trim description)))))
+  (let* ((uri (org-link-escape link))
+	 (zero-width-space (string ?\x200B))
+	 (description
+	  (and (org-string-nw-p description)
+	       ;; Description cannot contain two consecutive square
+	       ;; brackets, or end with a square bracket.  To prevent
+	       ;; this, insert a zero width space character between
+	       ;; the brackets, or at the end of the description.
+	       (replace-regexp-in-string
+		"\\(]\\)\\(]\\)"
+		(concat "\\1" zero-width-space "\\2")
+		(replace-regexp-in-string "]\\'"
+					  (concat "\\&" zero-width-space)
+					  (org-trim description))))))
     (format "[[%s]%s]"
 	    uri
 	    (if description (format "[%s]" description) ""))))
@@ -1207,7 +1222,7 @@ If there is no description, use the link target."
   (save-match-data
     (replace-regexp-in-string
      org-link-bracket-re
-     (lambda (m) (or (match-string 3 m) (match-string 1 m)))
+     (lambda (m) (or (match-string 2 m) (match-string 1 m)))
      s nil t)))
 
 (defun org-link-add-angle-brackets (s)
@@ -1662,7 +1677,7 @@ don't allow to edit the default description."
      ((org-in-regexp org-link-bracket-re 1)
       ;; We do have a link at point, and we are going to edit it.
       (setq remove (list (match-beginning 0) (match-end 0)))
-      (setq desc (when (match-end 3) (match-string-no-properties 3)))
+      (setq desc (when (match-end 2) (match-string-no-properties 2)))
       (setq link (read-string "Link: "
 			      (org-link-unescape
 			       (match-string-no-properties 1)))))

+ 3 - 5
lisp/org-agenda.el

@@ -5201,7 +5201,7 @@ function from a program - use `org-agenda-get-day-entries' instead."
     (when results
       (setq results
 	    (mapcar (lambda (i) (replace-regexp-in-string
-				 org-link-bracket-re "\\3" i)) results))
+				 org-link-bracket-re "\\2" i)) results))
       (concat (org-agenda-finalize-entries results) "\n"))))
 
 ;;; Agenda entry finders
@@ -6571,9 +6571,7 @@ Any match of REMOVE-RE will be removed from TXT."
 	      level (or level ""))
 	(if (string-match org-link-bracket-re category)
 	    (progn
-	      (setq l (if (match-end 3)
-			  (- (match-end 3) (match-beginning 3))
-			(- (match-end 1) (match-beginning 1))))
+	      (setq l (string-width (or (match-string 2) (match-string 1))))
 	      (when (< l (or org-prefix-category-length 0))
 		(setq category (copy-sequence category))
 		(org-add-props category nil
@@ -10238,7 +10236,7 @@ to override `appt-message-warning-time'."
      (lambda (x)
        (let* ((evt (org-trim
                     (replace-regexp-in-string
-                     org-link-bracket-re "\\3"
+                     org-link-bracket-re "\\2"
                      (or (get-text-property 1 'txt x) ""))))
               (cat (get-text-property (1- (length x)) 'org-category x))
               (tod (get-text-property 1 'time-of-day x))

+ 2 - 2
lisp/org-clock.el

@@ -2621,10 +2621,10 @@ from the dynamic block definition."
 		      (if (and (string-match
 				(format "\\`%s\\'" org-link-bracket-re)
 				headline)
-			       (match-end 3))
+			       (match-end 2))
 			  (format "[[%s][%s]]"
 				  (match-string 1 headline)
-				  (org-shorten-string (match-string 3 headline)
+				  (org-shorten-string (match-string 2 headline)
 						      narrow))
 			(org-shorten-string headline narrow))))
 	      (cl-flet ((format-field (f) (format (cond ((not emph) "%s |")

+ 0 - 3
lisp/org-compat.el

@@ -462,9 +462,6 @@ use of this function is for the stuck project list."
 (define-obsolete-variable-alias 'org-descriptive-links
   'org-link-descriptive "Org 9.3")
 
-(define-obsolete-variable-alias 'org-url-hexify-p
-  'org-link-url-hexify "Org 9.3")
-
 (define-obsolete-variable-alias 'org-context-in-file-links
   'org-link-context-for-files "Org 9.3")
 

+ 2 - 2
lisp/org-element.el

@@ -3141,8 +3141,8 @@ Assume point is at the beginning of the link."
        ;; Type 2: Standard link, i.e. [[https://orgmode.org][homepage]]
        ((looking-at org-link-bracket-re)
 	(setq format 'bracket)
-	(setq contents-begin (match-beginning 3))
-	(setq contents-end (match-end 3))
+	(setq contents-begin (match-beginning 2))
+	(setq contents-end (match-end 2))
 	(setq link-end (match-end 0))
 	;; RAW-LINK is the original link.  Decode any encoding.
 	;; Expand any abbreviation in it.

+ 6 - 11
lisp/org-mobile.el

@@ -31,14 +31,10 @@
 ;; iPhone and Android - any external viewer/flagging/editing
 ;; application that uses the same conventions could be used.
 
+(require 'cl-lib)
 (require 'org)
 (require 'org-agenda)
-(require 'cl-lib)
-
-(declare-function org-link-escape "ol" (text &optional table merge))
-(declare-function org-link-unescape "ol" (str))
-
-(defvar org-agenda-keep-restricted-file-list)
+(require 'ol)
 
 ;;; Code:
 
@@ -673,8 +669,7 @@ The table of checksums is written to the file mobile-checksums."
 	    (org-mobile-escape-olp (nth 4 (org-heading-components))))))
 
 (defun org-mobile-escape-olp (s)
-  (let  ((table '(?: ?/)))
-    (org-link-escape s table)))
+  (org-link-encode s '(?: ?/)))
 
 (defun org-mobile-create-sumo-agenda ()
   "Create a file that contains all custom agenda views."
@@ -968,7 +963,7 @@ is currently a noop.")
 	(if (not (string-match "\\`olp:\\(.*?\\)$" link))
 	    nil
 	  (let ((file (match-string 1 link)))
-	    (setq file (org-link-unescape file))
+	    (setq file (org-link-decode file))
 	    (setq file (expand-file-name file org-directory))
 	    (save-excursion
 	      (find-file file)
@@ -978,9 +973,9 @@ is currently a noop.")
 	      (point-marker))))
       (let ((file (match-string 1 link))
 	    (path (match-string 2 link)))
-	(setq file (org-link-unescape file))
+	(setq file (org-link-decode file))
 	(setq file (expand-file-name file org-directory))
-	(setq path (mapcar 'org-link-unescape
+	(setq path (mapcar #'org-link-decode
 			   (org-split-string path "/")))
 	(org-find-olp (cons file path))))))
 

+ 2 - 2
lisp/org-protocol.el

@@ -301,7 +301,7 @@ results of that splitting are returned as a list."
          (split-parts (split-string data sep)))
     (cond ((not unhexify) split-parts)
 	  ((fboundp unhexify) (mapcar unhexify split-parts))
-	  (t (mapcar #'org-link-unescape split-parts)))))
+	  (t (mapcar #'org-link-decode split-parts)))))
 
 (defun org-protocol-flatten-greedy (param-list &optional strip-path replacement)
   "Transform PARAM-LIST into a flat list for greedy handlers.
@@ -382,7 +382,7 @@ If INFO is already a property list, return it unchanged."
 	  (while data
 	    (setq result
 		  (append result
-			  (list (pop data) (org-link-unescape (pop data))))))
+			  (list (pop data) (org-link-decode (pop data))))))
 	  result)
       (let ((data (org-protocol-split-data info t org-protocol-data-separator)))
 	(if default-order

+ 1 - 1
lisp/org.el

@@ -9389,7 +9389,7 @@ prefix argument (`C-u C-u C-u C-c C-w')."
 			       (setq heading-text
 				     (replace-regexp-in-string
 				      org-link-bracket-re
-				      "\\3"
+				      "\\2"
 				      (or (nth 4 (org-heading-components))
 					  ""))))
 			     (org-refile-get-location

+ 4 - 4
testing/README

@@ -120,15 +120,15 @@ load and run the test suite with the following commands.
 
    To run one test: Use this as a demo example of a failing test
    #+BEGIN_SRC emacs-lisp
-     (ert-deftest test-org/org-link-escape-ascii-character-demo-of-fail ()
+     (ert-deftest test-org/org-link-encode-ascii-character-demo-of-fail ()
        (should (string= "%5B"  ; Expecting %5B is correct.
-                        (org-link-escape "[")))
+                        (org-link-encode "[")))
        (should (string= "%5C"  ; Expecting %5C is wrong, %5D correct.
-                        (org-link-escape "]"))))
+                        (org-link-encode "]"))))
    #+END_SRC
    or evaluate the ~ert-deftest form~ of the test you want to run.
    Then ~M-x ert RET
-   test-org/org-link-escape-ascii-character-demo-of-fail RET~.  When
+   test-org/org-link-encode-ascii-character-demo-of-fail RET~.  When
    not visible yet switch to the ERT results buffer named ~*ert*~.
    When a test failed the ERT results buffer shows the details of the
    first ~should~ that failed.  See ~(info "(ert)Running Tests

+ 92 - 66
testing/lisp/test-ol.el

@@ -20,80 +20,106 @@
 ;;; Code:
 
 
-;;; (Un)Escape links
+;;; Decode and Encode Links
 
-(ert-deftest test-ol/escape-ascii-character ()
-  "Escape an ascii character."
-  (should
-   (string=
-    "%5B"
-    (org-link-escape "["))))
-
-(ert-deftest test-ol/escape-ascii-ctrl-character ()
-  "Escape an ascii control character."
-  (should
-   (string=
-    "%09"
-    (org-link-escape "\t"))))
-
-(ert-deftest test-ol/escape-multibyte-character ()
-  "Escape an unicode multibyte character."
-  (should
-   (string=
-    "%E2%82%AC"
-    (org-link-escape "€"))))
-
-(ert-deftest test-ol/escape-custom-table ()
-  "Escape string with custom character table."
-  (should
-   (string=
-    "Foo%3A%42ar%0A"
-    (org-link-escape "Foo:Bar\n" '(?\: ?\B)))))
+(ert-deftest test-ol/encode ()
+  "Test `org-link-encode' specifications."
+  ;; Regural test.
+  (should (string= "Foo%3A%42ar" (org-link-encode "Foo:Bar" '(?\: ?\B))))
+  ;; Encode an ASCII character.
+  (should (string= "%5B" (org-link-encode "[" '(?\[))))
+  ;; Encode an ASCII control character.
+  (should (string= "%09" (org-link-encode "\t" '(9))))
+  ;; Encode a Unicode multibyte character.
+  (should (string= "%E2%82%AC" (org-link-encode "€" '(?\€)))))
 
-(ert-deftest test-ol/escape-custom-table-merge ()
-  "Escape string with custom table merged with default table."
-  (should
-   (string=
-    "%5BF%6F%6F%3A%42ar%0A%5D"
-    (org-link-escape "[Foo:Bar\n]" '(?\: ?\B ?\o) t))))
+(ert-deftest test-ol/decode ()
+  "Test `org-link-decode' specifications."
+  ;; Decode an ASCII character.
+  (should (string= "[" (org-link-decode "%5B")))
+  ;; Decode an ASCII control character.
+  (should (string= "\n" (org-link-decode "%0A")))
+  ;; Decode a Unicode multibyte character.
+  (should (string= "€" (org-link-decode "%E2%82%AC"))))
 
-(ert-deftest test-ol/unescape-ascii-character ()
-  "Unescape an ascii character."
+(ert-deftest test-ol/encode-url-with-escaped-char ()
+  "Encode and decode a URL that includes an encoded char."
   (should
-   (string=
-    "["
-    (org-link-unescape "%5B"))))
+   (string= "http://some.host.com/form?&id=blah%2Bblah25"
+	    (org-link-decode
+	     (org-link-encode "http://some.host.com/form?&id=blah%2Bblah25"
+			      '(?\s ?\[ ?\] ?%))))))
 
-(ert-deftest test-ol/unescape-ascii-ctrl-character ()
-  "Unescpae an ascii control character."
-  (should
-   (string=
-    "\n"
-    (org-link-unescape "%0A"))))
+
+;;; Escape and Unescape Links
 
-(ert-deftest test-ol/unescape-multibyte-character ()
-  "Unescape unicode multibyte character."
-  (should
-   (string=
-    "€"
-    (org-link-unescape "%E2%82%AC"))))
+(ert-deftest test-ol/escape ()
+  "Test `org-link-escape' specifications."
+  ;; No-op when there is no backslash or closing square bracket.
+  (should (string= "foo[" (org-link-escape "foo[")))
+  ;; Escape closing square bracket at the end of the link.
+  (should (string= "[foo\\]" (org-link-escape "[foo]")))
+  ;; Escape closing square brackets followed by another square
+  ;; bracket.
+  (should (string= "foo\\][bar" (org-link-escape "foo][bar")))
+  (should (string= "foo\\]]bar" (org-link-escape "foo]]bar")))
+  ;; However, escaping closing square bracket at the end of the link
+  ;; has precedence over the previous rule.
+  (should (string= "foo]\\]" (org-link-escape "foo]]")))
+  ;; Escape backslashes at the end of the link.
+  (should (string= "foo\\\\" (org-link-escape "foo\\")))
+  ;; Escape backslashes that could be confused with escaping
+  ;; characters.
+  (should (string= "foo\\\\\\]" (org-link-escape "foo\\]")))
+  (should (string= "foo\\\\\\][" (org-link-escape "foo\\][")))
+  (should (string= "foo\\\\\\]]bar" (org-link-escape "foo\\]]bar")))
+  ;; Do not escape backslash characters when unnecessary.
+  (should (string= "foo\\bar" (org-link-escape "foo\\bar")))
+  (should (string= "foo\\]bar" (org-link-escape "foo\\]bar")))
+  ;; Pathological cases: consecutive closing square brackets.
+  (should (string= "[[[foo\\]]\\]" (org-link-escape "[[[foo]]]")))
+  (should (string= "[[[foo]\\]] bar" (org-link-escape "[[[foo]]] bar"))))
 
-(ert-deftest test-ol/unescape-ascii-extended-char ()
-  "Unescape old style percent escaped character."
-  (should
-   (string=
-    "àâçèéêîôùû"
-        (decode-coding-string
-	 (org-link-unescape "%E0%E2%E7%E8%E9%EA%EE%F4%F9%FB") 'latin-1))))
+(ert-deftest test-ol/unescape ()
+  "Test `org-link-unescape' specifications."
+  ;; No-op if there is no backslash.
+  (should (string= "foo[" (org-link-unescape "foo[")))
+  ;; No-op if backslashes are not escaping backslashes.
+  (should (string= "foo\\bar" (org-link-unescape "foo\\bar")))
+  (should (string= "foo\\]bar" (org-link-unescape "foo\\]bar")))
+  ;;
+  (should (string= "foo\\]" (org-link-unescape "foo\\\\\\]")))
+  (should (string= "foo\\][" (org-link-unescape "foo\\\\\\][")))
+  (should (string= "foo\\]]bar" (org-link-unescape "foo\\\\\\]]bar")))
+  ;; Unescape backslashes at the end of the link.
+  (should (string= "foo\\" (org-link-unescape "foo\\\\")))
+  ;; Unescape closing square bracket at the end of the link.
+  (should (string= "[foo]" (org-link-unescape "[foo\\]")))
+  ;; Pathological cases: consecutive closing square brackets.
+  (should (string= "[[[foo]]]" (org-link-unescape "[[[foo\\]]\\]")))
+  (should (string= "[[[foo]]] bar" (org-link-unescape "[[[foo]\\]] bar"))))
 
-(ert-deftest test-ol/escape-url-with-escaped-char ()
-  "Escape and unescape a URL that includes an escaped char.
-http://article.gmane.org/gmane.emacs.orgmode/21459/"
-  (should
-   (string=
-    "http://some.host.com/form?&id=blah%2Bblah25"
-    (org-link-unescape
-     (org-link-escape "http://some.host.com/form?&id=blah%2Bblah25")))))
+(ert-deftest test-ol/make-string ()
+  "Test `org-link-make-string' specifications."
+  ;; Throw an error on empty URI.
+  (should-error (org-link-make-string ""))
+  ;; Empty description returns a [[URI]] construct.
+  (should (string= "[[uri]]"(org-link-make-string "uri")))
+  ;; Non-empty description returns a [[URI][DESCRIPTION]] construct.
+  (should
+   (string= "[[uri][description]]"
+	    (org-link-make-string "uri" "description")))
+  ;; Escape "]]" strings in the description with zero-width spaces.
+  (should
+   (let ((zws (string ?\x200B)))
+     (string= (format "[[uri][foo]%s]bar]]" zws)
+	      (org-link-make-string "uri" "foo]]bar"))))
+  ;; Prevent description from ending with a closing square bracket
+  ;; with a zero-width space.
+  (should
+   (let ((zws (string ?\x200B)))
+     (string= (format "[[uri][foo]%s]]" zws)
+	      (org-link-make-string "uri" "foo]")))))
 
 
 ;;; Store links

+ 18 - 18
testing/lisp/test-org-clock.el

@@ -590,18 +590,18 @@ CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
   ;; Otherwise, link to the headline in the current file.
   (should
    (equal
-    "| Headline     | Time    |
-|--------------+---------|
-| *Total time* | *26:00* |
-|--------------+---------|
-| [[file:filename::Foo][Foo]]          | 26:00   |"
+    "| Headline                      | Time    |
+|-------------------------------+---------|
+| *Total time*                  | *26:00* |
+|-------------------------------+---------|
+| [[file:filename::Foo][Foo]] | 26:00   |"
     (org-test-with-temp-text-in-file
         "* Foo
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
       (let ((file (buffer-file-name)))
         (replace-regexp-in-string
          (regexp-quote file) "filename"
-         (test-org-clock-clocktable-contents ":link t"))))))
+         (test-org-clock-clocktable-contents ":link t :lang en"))))))
   ;; Ignore TODO keyword, priority cookie, COMMENT and tags in
   ;; headline.
   (should
@@ -675,26 +675,26 @@ CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
   ;; links if there is no description.
   (should
    (equal
-    "| Headline     | Time    |
-|--------------+---------|
-| *Total time* | *26:00* |
-|--------------+---------|
-| [[Foo %5B%5Bhttps://orgmode.org%5D%5BOrg mode%5D%5D][Foo Org mode]] | 26:00   |"
+    "| Headline                                | Time    |
+|-----------------------------------------+---------|
+| *Total time*                            | *26:00* |
+|-----------------------------------------+---------|
+| [[Foo [[https://orgmode.org\\][Org mode]\\]][Foo Org mode]] | 26:00   |"
     (org-test-with-temp-text
         "* Foo [[https://orgmode.org][Org mode]]
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
-      (test-org-clock-clocktable-contents ":link t"))))
+      (test-org-clock-clocktable-contents ":link t :lang en"))))
   (should
    (equal
-    "| Headline                | Time    |
-|-------------------------+---------|
-| *Total time*            | *26:00* |
-|-------------------------+---------|
-| [[Foo %5B%5Bhttps://orgmode.org%5D%5D][Foo https://orgmode.org]] | 26:00   |"
+    "| Headline                     | Time    |
+|------------------------------+---------|
+| *Total time*                 | *26:00* |
+|------------------------------+---------|
+| [[Foo [[https://orgmode.org]\\]][Foo https://orgmode.org]] | 26:00   |"
     (org-test-with-temp-text
         "* Foo [[https://orgmode.org]]
 CLOCK: [2016-12-27 Wed 13:09]--[2016-12-28 Wed 15:09] => 26:00"
-      (test-org-clock-clocktable-contents ":link t")))))
+      (test-org-clock-clocktable-contents ":link t :lang en")))))
 
 (ert-deftest test-org-clock/clocktable/compact ()
   "Test \":compact\" parameter in Clock table."

+ 3 - 7
testing/lisp/test-org.el

@@ -2331,7 +2331,7 @@ SCHEDULED: <2014-03-04 tue.>"
   ;; Handle escape characters.
   (should
    (org-test-with-temp-text
-       "* H1\n:PROPERTIES:\n:CUSTOM_ID: [%]\n:END:\n* H2\n[[#%5B%25%5D<point>]]"
+       "* H1\n:PROPERTIES:\n:CUSTOM_ID: [%]\n:END:\n* H2\n[[#[%\\]<point>]]"
      (org-open-at-point)
      (looking-at-p "\\* H1")))
   ;; Throw an error on false positives.
@@ -2425,13 +2425,9 @@ Foo Bar
    (org-test-with-temp-text "[[*Test]]\n* TODO COMMENT Test"
      (org-open-at-point)
      (looking-at "\\* TODO COMMENT Test")))
-  ;; Correctly un-hexify fuzzy links.
+  ;; Correctly un-escape fuzzy links.
   (should
-   (org-test-with-temp-text "* With space\n[[*With%20space][With space<point>]]"
-     (org-open-at-point)
-     (bobp)))
-  (should
-   (org-test-with-temp-text "* [1]\n[[*%5B1%5D<point>]]"
+   (org-test-with-temp-text "* [foo]\n[[*[foo\\]][With escaped characters]]"
      (org-open-at-point)
      (bobp)))
   ;; Match search strings containing newline characters, including

+ 2 - 2
testing/lisp/test-ox.el

@@ -3510,9 +3510,9 @@ Another text. (ref:text)
 	 (org-element-type
 	  (org-export-resolve-fuzzy-link
 	   (org-element-map tree 'link 'identity info t) info)))))
-  ;; Handle url-encoded fuzzy links.
+  ;; Handle escaped fuzzy links.
   (should
-   (org-test-with-parsed-data "* A B\n[[A%20B]]"
+   (org-test-with-parsed-data "* [foo]\n[[[foo\\]]]"
      (org-export-resolve-fuzzy-link
       (org-element-map tree 'link #'identity info t) info))))