On 2025-07-27 18:51, Ihor Radchenko wrote:
> Jens Schmidt <jschmidt4...@vodafonemail.de> writes:
>
>> It's been a while since my question on this mailing list about block
>> boundaries [1]. For my private code I came up with the function below
>> to both get and test on all sorts of block boundaries. Like this:
>>
>> (and-let* ((element (or element (org-element-at-point)))
>> ;; returns nil if point is outside of markup
>> (boundaries (org-element-boundaries element 'markup t))
>> (start (car boundaries))
>> (end (cdr boundaries)))
>> ...)
>>
>> API and code are surely rather sound than ingenious, but if you
>> consider it (or some variation of it) useful for inclusion into Org
>> mode, just let me know, I'd prepare a proper patch plus tests plus ...
>>
>> The second part of the code is for interactive testing only.
>>
>> If you feel I should provide examples where this could be actually
>> used in Org's code base, just let me know - I can set up some example
>> patches as well.
>
> Having example would help.
> [...]
Sorry for the late reply. Have been busy and I wanted to provide
real-life examples. See attached patch.
Some general notes:
- This is not a "real" patch I'm asking to include, just an example.
In particular, I'm not asking to use the new function to replace
existing code.
- The modifications are not always one-to-one, and I haven't checked
them for bug-compatibility.
- But in general, I hope they show what `org-element-boundaries' can
do. At least I find the new code more readable, but that's in the
eye of the beholder, as always.
- To find the examples, I searched (rather lazily and only in two or
three sources) for "skip-syntax-backward". These are usually good
replacement candidates.
Please let me know what you think.
Thanks, as always, for your work as Org maintainer!
Jens
From 381737e75a02fafb40f4cd766ffe54a0c0e2c4f7 Mon Sep 17 00:00:00 2001
From: Jens Schmidt <farb...@vodafonemail.de>
Date: Fri, 15 Aug 2025 23:04:29 +0200
Subject: [PATCH] Provide `org-element-boundaries' and nonbinding, real-life
examples.
From: Jens Schmidt <jschmidt4...@vodafonemail.de>
---
lisp/ob-core.el | 79 ++++++++++++++++---------------
lisp/org-element.el | 113 ++++++++++++++++++++++++++++++++++++++++++++
lisp/org.el | 51 +++++++-------------
3 files changed, 171 insertions(+), 72 deletions(-)
diff --git a/lisp/ob-core.el b/lisp/ob-core.el
index 326a9a857..bef28fce3 100644
--- a/lisp/ob-core.el
+++ b/lisp/ob-core.el
@@ -1984,14 +1984,13 @@ If the point is not on a source block or within blank lines after an
src block, then return nil."
(let ((element (or src-block (org-element-at-point))))
(when (org-element-type-p element 'src-block)
- (let ((end (org-element-end element)))
- (org-with-wide-buffer
- ;; Ensure point is not on a blank line after the block.
- (forward-line 0)
- (skip-chars-forward " \r\t\n" end)
- (when (< (point) end)
- (prog1 (goto-char (org-element-post-affiliated element))
- (looking-at org-babel-src-block-regexp))))))))
+ ;; Here (and further down) I made a feeble attempt to get rid of
+ ;; references to `org-babel-src-block-regexp' and this function
+ ;; itself. With some more effort it should be possible...
+ (let ((boundaries (org-element-boundaries element 'markup)))
+ (when (<= (point) (cdr boundaries))
+ (prog1 (goto-char (car boundaries))
+ (looking-at org-babel-src-block-regexp)))))))
;;;###autoload
(defun org-babel-goto-src-block-head ()
@@ -2120,13 +2119,20 @@ With optional prefix argument ARG, jump backward ARG many source blocks."
(defun org-babel-mark-block ()
"Mark current source block."
(interactive)
- (let ((head (org-babel-where-is-src-block-head)))
- (when head
- (save-excursion
- (goto-char head)
- (looking-at org-babel-src-block-regexp))
- (push-mark (match-end 5) nil t)
- (goto-char (match-beginning 5)))))
+ ;; This requires two calls to `org-element-boundaries' on the
+ ;; element, one to check whether we are in the range of its markup
+ ;; and one to determine the boundaries of its contents.
+ ;;
+ ;; As a perk, one gets a user error instead of a no-op if not on a
+ ;; source block.
+ (if-let* ((element (org-element-at-point))
+ ((org-element-type-p element 'src-block))
+ ((org-element-boundaries element 'markup 'inclusive))
+ (contents (org-element-boundaries element 'contents)))
+ (progn
+ (push-mark (1+ (cdr contents)) nil t)
+ (goto-char (car contents)))
+ (user-error "Not in a source block")))
(defun org-babel-demarcate-block (&optional arg)
"Wrap or split the code in an active region or at point.
@@ -2988,28 +2994,27 @@ used as a string to be appended to #+begin_example line."
(defun org-babel-update-block-body (new-body)
"Update the body of the current code block to NEW-BODY."
- (let ((element (org-element-at-point)))
- (unless (org-element-type-p element 'src-block)
- (error "Not in a source block"))
- (goto-char (org-babel-where-is-src-block-head element))
- (let* ((ind (org-current-text-indentation))
- (body-start (line-beginning-position 2))
- (body (org-element-normalize-string
- (if (org-src-preserve-indentation-p element) new-body
- (with-temp-buffer
- (insert (org-remove-indentation new-body))
- (indent-rigidly
- (point-min)
- (point-max)
- (+ ind org-edit-src-content-indentation))
- (buffer-string))))))
- (delete-region body-start
- (org-with-wide-buffer
- (goto-char (org-element-end element))
- (skip-chars-backward " \t\n")
- (line-beginning-position)))
- (goto-char body-start)
- (insert body))))
+ ;; Again, two calls to `org-element-boundaries'.
+ (if-let* ((element (org-element-at-point))
+ ((org-element-type-p element 'src-block))
+ (markup (org-element-boundaries element 'markup 'inclusive)))
+ (let* ((ind (progn
+ (goto-char (car markup))
+ (org-current-text-indentation)))
+ (body (org-element-normalize-string
+ (if (org-src-preserve-indentation-p element) new-body
+ (with-temp-buffer
+ (insert (org-remove-indentation new-body))
+ (indent-rigidly
+ (point-min)
+ (point-max)
+ (+ ind org-edit-src-content-indentation))
+ (buffer-string)))))
+ (contents (org-element-boundaries element 'contents)))
+ (delete-region (car contents) (1+ (cdr contents)))
+ (goto-char (car contents))
+ (insert body))
+ (error "Not in a source block")))
(defun org-babel-merge-params (&rest alists)
"Combine all parameter association lists in ALISTS.
diff --git a/lisp/org-element.el b/lisp/org-element.el
index 062141fce..f5a0269db 100644
--- a/lisp/org-element.el
+++ b/lisp/org-element.el
@@ -8739,6 +8739,119 @@ end of ELEM-A."
(org-fold-core-regions (cdr folds) :relative beg-A)
(goto-char (org-element-end elem-B))))))
+;;;###autoload
+(defun org-element-boundaries (element boundary-type &optional point-test)
+ "Return a cons (START . END) of the boundaries of ELEMENT.
+Determine START and END depending on BOUNDARY-TYPE, which can be any of the
+following symbols:
+
+`element', `gross-elt': Return the outer element boundaries, which include
+leading affiliated keywords and trailing blank lines [1].
+`markup', `net-elt': Return the boundaries of the element sans affiliated
+keywords and trailing blank lines. For block-like elements this coincides
+with the markup boundaries [2].
+
+For block-like elements, that is, elements having separate markup lines
+like source blocks, the following boundary types are also available:
+
+`contents', `gross-contents': Return the boundaries of the block contents
+[3].
+`net-contents': Return the boundaries of the block contents sans leading
+and trailing whitespace (' \\t\\n\\r') [4].
+
+Here is an example block that shows the boundaries as returned by this
+function (leading and trailing blanks denoted with underscores):
+
+[1...#+name: foo
+[2...#+begin_example
+[3...__[4...foo
+bar
+__baz...4]__
+__...3]
+#+end_example__...2]
+__...1]
+Bar.
+
+BOUNDARY-TYPE can also be a cons (START-TYPE . END-TYPE) to determine START
+and END with respect to different boundary types.
+
+For certain combinations of contents-based boundary types and degenerate
+blocks, the literal interpretation of above rules could result in END being
+smaller than START. In these cases this function returns START and END
+equal to CONTENTS-START, where CONTENTS-START is the position of the
+beginning of line after the opening markup line.
+
+Except for that special case and boundary type `net-contents' in general,
+START is always at the beginning and END is always at the end of a line.
+START is always smaller than or equal to END.
+
+If optional parameter POINT-TEST equals `exclusive', this function returns
+nil instead of the cons if point is not exclusively/strictly between START
+and END. Any other non-nil value changes that to a non-exclusive test,
+where point is allowed to equal START or END. POINT-TEST can also be a
+cons (START-TEST . END-TEST) to test START or END vs. point with respect to
+different values of strictness."
+ (let* ((start-type (or (car-safe boundary-type) boundary-type))
+ (end-type (or (cdr-safe boundary-type) boundary-type))
+ (start-test (if (consp point-test) (car point-test) point-test))
+ (end-test (if (consp point-test) (cdr point-test) point-test))
+ (point (point))
+ start end contents-start)
+ (org-with-wide-buffer
+ (setq start
+ (pcase-exhaustive start-type
+ ((or 'element 'gross-elt)
+ (org-element-begin element))
+ ((or 'markup 'net-elt)
+ (org-element-post-affiliated element))
+ ((or 'contents 'gross-contents)
+ (goto-char (org-element-post-affiliated element))
+ (forward-line 1)
+ (setq contents-start (point))
+ (point))
+ ('net-contents
+ (goto-char (org-element-post-affiliated element))
+ (forward-line 1)
+ (setq contents-start (point))
+ (skip-chars-forward " \t\n\r")
+ (point))))
+ (setq end
+ (progn
+ (goto-char (org-element-end element))
+ (pcase-exhaustive end-type
+ ;; adjust element end depending on the
+ ;; surrounding or following element
+ ((or 'element 'gross-elt)
+ (if (looking-at "[ \t\n\r]*$")
+ (line-end-position 1)
+ (line-end-position 0)))
+ ((or 'markup 'net-elt)
+ (skip-chars-backward " \t\n\r")
+ (line-end-position 1))
+ ((or 'contents 'gross-contents)
+ (skip-chars-backward " \t\n\r")
+ (line-end-position 0))
+ ('net-contents
+ (skip-chars-backward " \t\n\r")
+ (forward-line 0)
+ (skip-chars-backward " \t\n\r")
+ (point)))))
+ ;; handle empty or whitespace-only blocks. CONTENTS-START
+ ;; is nil if START-TYPE is not contents-based. Which is OK,
+ ;; since in that case END is larger than START.
+ (when (< end start)
+ (setq start contents-start
+ end contents-start))
+ (and (cond ((not start-test))
+ ((eq start-test 'exclusive)
+ (< start point))
+ ((<= start point)))
+ (cond ((not end-test))
+ ((eq end-test 'exclusive)
+ (< point end))
+ ((<= point end)))
+ (cons start end)))))
+
(provide 'org-element)
;; Local variables:
diff --git a/lisp/org.el b/lisp/org.el
index 65abfbe1a..46cdca0bd 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -7194,12 +7194,12 @@ Assume point is at a heading or an inlinetask beginning."
(indent-line-to (+ (current-indentation) diff))
(forward-line 0)
(or (and (looking-at-p "[ \t]*#\\+BEGIN_\\(EXAMPLE\\|SRC\\)")
- (let ((e (org-element-at-point)))
- (and (org-src-preserve-indentation-p e)
- (goto-char (org-element-end e))
- (progn (skip-chars-backward " \r\t\n")
- (forward-line 0)
- t))))
+ ;; Here I need only one end of the boundary. To
+ ;; me it still seems more intelligible, anyway.
+ (let* ((e (org-element-at-point))
+ (c (org-element-boundaries e 'contents)))
+ (when (org-src-preserve-indentation-p e)
+ (goto-char (1+ (cdr c))))))
(forward-line)))))))))
(defun org-convert-to-odd-levels ()
@@ -11119,14 +11119,12 @@ POS may also be a marker."
(with-current-buffer (if (markerp pos) (marker-buffer pos) (current-buffer))
(org-with-wide-buffer
(goto-char pos)
- (let ((drawer (org-element-at-point)))
+ (let ((drawer (org-element-at-point)) markup)
(when (and (org-element-type-p drawer '(drawer property-drawer))
(not (org-element-contents-begin drawer)))
- (delete-region (org-element-begin drawer)
- (progn (goto-char (org-element-end drawer))
- (skip-chars-backward " \r\t\n")
- (forward-line)
- (point))))))))
+ ;; An easy one ...
+ (setq markup (org-element-boundaries drawer 'markup))
+ (delete-region (car markup) (1+ (cdr markup))))))))
(defvar org-ts-type nil)
(defun org-sparse-tree (&optional arg type)
@@ -19039,14 +19037,9 @@ part of a source block.
When ELEMENT is provided, it is considered to be element at point."
(save-match-data (setq element (or element (org-element-at-point))))
- (when (org-element-type-p element 'src-block)
- (or (not inside)
- (not (or (<= (line-beginning-position)
- (org-element-post-affiliated element))
- (>= (line-end-position)
- (org-with-point-at (org-element-end element)
- (skip-chars-backward " \t\n\r")
- (point))))))))
+ ;; ... and an even more easy one ...
+ (and (org-element-type-p element 'src-block)
+ (org-element-boundaries element (if inside 'contents 'element) 'inclusive)))
(defun org-context ()
"Return a list of contexts of the current cursor position.
@@ -19480,10 +19473,7 @@ ELEMENT."
(memq type '(comment-block example-block export-block
src-block verse-block)))
(let ((cend (or (org-element-contents-end element)
- (org-with-wide-buffer
- (goto-char (org-element-end element))
- (skip-chars-backward " \r\t\n")
- (line-beginning-position)))))
+ (1+ (cdr (org-element-boundaries element 'contents))))))
(and cend (<= cend pos))))
;; As a special case, if point is at the end of a footnote
;; definition or an item, indent like the very last element
@@ -19576,20 +19566,11 @@ Also align node properties according to `org-property-format'."
(org-element-post-affiliated element)))
nil)
((and (eq type 'latex-environment)
- (>= (point) (org-element-post-affiliated element))
- (< (point)
- (org-with-point-at (org-element-end element)
- (skip-chars-backward " \t\n")
- (line-beginning-position 2))))
+ (org-element-boundaries element 'markup 'inclusive))
nil)
((and (eq type 'src-block)
org-src-tab-acts-natively
- (> (line-beginning-position)
- (org-element-post-affiliated element))
- (< (line-beginning-position)
- (org-with-point-at (org-element-end element)
- (skip-chars-backward " \t\n")
- (line-beginning-position))))
+ (org-element-boundaries element 'contents 'inclusive))
(let ((block-content-ind
(when (not (org-src-preserve-indentation-p element))
(org-with-point-at (org-element-property :begin element)
--
2.39.5