Hello Rudy,
please find here the latest patch.
I tried to implement the changes request. This one is now simplified.
However: I could not implement this part. I tried a few approaches but the
result wasn't as clean as the current approach.
> I see. Why not match the string exactly once, capturing everything
> before, within, and after startAAA and startBBB, and then check if the
> captured AAA equals BBB, as mentioned before?
The current approach uses the let* in the beginning to set multiple variables
(where new variables depend on the ones before it). If any of the vars cannot
be set they will be nil.
After this initialization of vars we just check if the last var is nil (meaning
no surrounding start/end keywords could be found) so we expand the whole body
and then surround it with @startuml/@enduml and return it. If the last var is
not nil this means that surrounding start/end keywords were found so we just
expand the inner-body and return the correctly put back together body (with
pre-body, start-keyword, expanded-inner-body, end-keyword and post-body).
I think this is very clean. If you have anything to change please provide me
with complete alternative code. I hope however this one works for you.
Best
Tim
On 4 Oct 2025, at 11:44, Tim Hansinger wrote:
> Hello Rudy,
>
> thanks for the input.
>
>> I see. Why not match the string exactly once, capturing everything
>> before, within, and after startAAA and startBBB, and then check if the
>> captured AAA equals BBB, as mentioned before?
>
> I guess we could do that. So we just search for the first @start.* and the
> first @end.* (not matching the @end type to the @start type during this
> initial search) and then comparing the type-part after as you suggest. I
> checked the preprocessor code from plantuml and I guess that makes sense
> since the preprocessor also works like that (doesn't really allow
> @end<some_text_not_used_by_any_plantuml_type> ... @end seems to be always
> "protected" variables which can't/shouldn't be used in any other context).
>
> Initially I was thinking that it should be possible to handle this example:
>
> @startregex
> title literalText
> @endSomeNameNotBeingUsedByPlant
> @endregex
>
> But I rechecked it and the processor of plant doesn't like it.
>
> So by simplifying this we would automatically reduce the amount of "matches"
> in the code. I will propose something beginning of next week and if you see
> potential to improve just send me the code I should use instead. But wait for
> the next proposal :-)
>
>> I see. That is actually not a bad idea! But, we must call it something
>> else, like pre-body' and post-body', to avoid confusion.
>
> I will rename the parts.
>
>> On that topic, we need to refactor for readability, that is simplicity,
>> starting by the removal of all setq' and repeated when'.
>
> The when parts at the moment do different things/different matches... as
> mentioned before we should have less whens by simplifying the @start @end
> logic.
>
> Concerning the removal of setq:
> At the moment the first let* initializes the local variables: start end
> unwrapped-body expanded-body ... then later the setq sets these local
> variables to a value (or they stay nil).
>
> So setq purely acts on these local vars.
>
> Then in the end I check if wrapped-body is not nil and return the final body.
> I will simplify it as you ask for.
>
>> [...] Perhaps we could write `inner-body'?
>
> Sure. I can change it to inner-body.
>
> One other thing (only side-related)
>
> I read on the plantuml page this here (this recommendation exists since a few
> years):
>
> https://plantuml.com/preprocessing
>
>> Migration notes
>> The current preprocessor is an update from some legacy preprocessor.
>> Even if some legacy features are still supported with the actual
>> preprocessor, you should not use them any more (they might be removed in
>> some long term future).
>>
>> - You should not use !define and !definelong anymore. Use !function,
>> !procedure or variable definition instead.
>>
>> - !define should be replaced by return !function
>>
>> [...]
>
> This would mean that also the function
> org-babel-variable-assignments:plantuml (the one that builds currently the
> !define %s %s parts) could need some refactoring.
>
> Instead of !define foo bar we should use the new variable definition: !$foo =
> "bar".
>
> But this would also mean the following breaking change:
> When using the var inside the body currently users write just foo ... they
> would now need to change all of the foo to @foo.
>
> If you want I could open a new issue for that (to not get off topic in this
> discussion). How are such things typically handled (recommended migrations to
> prevent possible future breakage with third party programs)?
>
> Best
> Tim
>
> On 3 Oct 2025, at 22:47, Rudolf Adamkovič wrote:
>
>> Tim Hansinger [email protected] writes:
>>
>>> Here I am searching for the @start<XXX> blocks.
>>> It will also match e.g. (where _ represents spaces or tabs):
>>> @start<XXX>__
>>>
>>> It will filter out the start-type (the XXX part) and then use this
>>> start-type to get the exactly matching end-type (@end<XXX>) ... so
>>> @end<ZZZ> would e.g. not match.
>>
>> I see. Why not match the string exactly once, capturing everything
>> before, within, and after startAAA and startBBB, and then check if the
>> captured AAA equals BBB, as mentioned before?
>>
>>> We now use the positions of these start/end items to grab whatever is
>>> BEFORE the @start<XXX> which I called in my code prologue, but it is
>>> not the prologue that comes from header arguments... it could be
>>> e.g. when someone writes a comment before @start<XXX>. It will also
>>> extract the body in between and the epilogue (again this is not the
>>> epilogue that comes from the header arguments... it is just
>>> "stuff/text" that someone might have written after the @end<XXX>
>>> block).
>>>
>>> I can through out/remove the pro and epi parts as we probably don't
>>> need them, I just thought you wanted them in there which seems to have
>>> been a misunderstanding.
>>
>> I see. That is actually not a bad idea! But, we must call it something
>> else, like pre-body' and post-body', to avoid confusion.
>>
>>> Then the extracted body in between the @start<XXX> and @end<XXX> lines
>>> (or the whole body if no matching @start/@end are found) is sent /
>>> expanded by org-babel-expand-body:generic as it should.
>>>
>>> In the end we then simply put the single elements back together into
>>> the final complete body... here I am just removing empty list items
>>> before joining them so no unnecessary newlines are created if e.g. the
>>> expanded-body is empty.
>>>
>>> So my code is not doing anything that is and rightfully should be done
>>> by org-babel-expand-body:generic.
>>
>> My bad, I misread the code.
>>
>> On that topic, we need to refactor for readability, that is simplicity,
>> starting by the removal of all setq' and repeated when'.
>>
>>> And you are correct, I should also rename wrapped-body to unwrapped-body.
>>
>> That was my mistake, BTW. Perhaps we could write `inner-body'?
>>
>> Rudy
>>
>> "Great minds discuss ideas; average minds discuss events; small minds
>> discuss people." --- Anna Eleanor Roosevelt (1884-1962)
>>
>> Rudolf Adamkovič [email protected] [he/him]
>> http://adamkovic.org
From f9c50630267a19fe0f5ae6455a113fbed9fe16ef Mon Sep 17 00:00:00 2001
From: Tim Hansinger <[email protected]>
Date: Mon, 15 Sep 2025 17:52:15 +0200
Subject: [PATCH] ob-plantuml.el: Fix body parsing to handle @start/@end
keywords
* lisp/ob-plantuml.el (org-babel-plantuml-make-body):
Refactor org-babel-plantuml-make-body to properly parse and preserve
content around PlantUML diagram blocks.
Previously, the function would apply variable expansion to the entire
body content and only check for @start keyword to decide whether to
wrap with @startuml/@enduml tags.
Changes:
- Parse body into 5 components: pre-body, start-keyword, inner-body,
end-keyword, and post-body
- Only apply variable expansion to the inner-body, not the entire body
- Require exact matching between @start<type> and @end<type> keywords
(e.g., @startuml must pair with @enduml, not @endsalt)
- If @start<type> exists but no matching @end<type> is found, treat
the entire body as diagram content and wrap with default keywords
- Preserve any content before the first @start keyword (pre-body) and
after the last @end keyword (post-body) without modification
(except trimming)
This ensures variable substitution only affects the actual diagram
content.
Examples now supported:
- Content before/after diagram blocks
- Proper handling of mismatched start/end keyword pairs
* testing/lisp/test-ob-plantuml.el
(test-ob-plantuml/single-var-on-body-with-start-end-keywords-and-content-outside-the-keywords):
Added an additional test for aboves use case (var definition on a BODY
with pre-existing @startxxx ... @endxxx keywords and pre-body / post-body
content).
Reported-by: "Tim Hansinger" <[email protected]>
Link:
https://list.orgmode.org/[email protected]/
---
lisp/ob-plantuml.el | 48 ++++++++++++++++++++++++++------
testing/lisp/test-ob-plantuml.el | 30 ++++++++++++++++++++
2 files changed, 70 insertions(+), 8 deletions(-)
diff --git a/lisp/ob-plantuml.el b/lisp/ob-plantuml.el
index 05a4f7263..e579344cf 100644
--- a/lisp/ob-plantuml.el
+++ b/lisp/ob-plantuml.el
@@ -95,20 +95,52 @@ are expected to be scalar variables."
(defun org-babel-plantuml-make-body (body params)
"Return PlantUML input string.
-
BODY is the content of the source block and PARAMS is a property list
of source block parameters. This function relies on the
`org-babel-expand-body:generic' function to extract `:var' entries
from PARAMS and on the `org-babel-variable-assignments:plantuml'
function to convert variables to PlantUML assignments.
-If BODY does not contain @startXXX ... @endXXX clauses, @startuml
-... @enduml will be added."
- (let ((full-body
- (org-babel-expand-body:generic
- body params (org-babel-variable-assignments:plantuml params))))
- (if (string-prefix-p "@start" body t) full-body
- (format "@startuml\n%s\n@enduml" full-body))))
+The function parses BODY to find matching @startXXX ... @endXXX keyword pairs:
+- If a matching pair is found (e.g., @startuml/@enduml or @startsalt/@endsalt),
+ variable expansion is applied only to the content between the keywords,
+ preserving any content outside the keywords.
+- If @startXXX is found but no matching @endXXX, or if no @startXXX is found,
+ the entire BODY is treated as diagram content and wrapped with
+ @startuml ... @enduml keywords after variable expansion."
+ (let* ((start-regex "^[[:blank:]]*@start\\([a-zA-Z]+\\)[[:blank:]]*$")
+ (start-match-pos (string-match start-regex body))
+ (start-match-end (and start-match-pos
+ (match-end 0)))
+ (start-type (and start-match-pos
+ (match-string 1 body)))
+ (end-regex (and start-type
+ (format "^[[:blank:]]*@end%s\\(?:[[:blank:]]*$\\)"
start-type)))
+ (end-match-pos (and end-regex
+ (string-match end-regex body start-match-end)))
+ (end-match-end (and end-match-pos
+ (match-end 0))))
+ (if end-match-end
+ (let ((pre-body (string-trim (substring body 0 start-match-pos)))
+ (start-keyword (string-trim (substring body start-match-pos
start-match-end)))
+ (expanded-inner-body (org-babel-expand-body:generic
+ (string-trim (substring body
start-match-end end-match-pos))
+ params
+ (org-babel-variable-assignments:plantuml
params)))
+ (end-keyword (string-trim (substring body end-match-pos
end-match-end)))
+ (post-body (string-trim (substring body end-match-end))))
+ (string-join (remove "" (list pre-body
+ start-keyword
+ expanded-inner-body
+ end-keyword
+ post-body)) "\n"))
+ (let ((start-keyword "@startuml")
+ (expanded-body (org-babel-expand-body:generic (string-trim body)
+ params
(org-babel-variable-assignments:plantuml params)))
+ (end-keyword "@enduml"))
+ (string-join (remove "" (list start-keyword
+ expanded-body
+ end-keyword)) "\n")))))
(defun org-babel-execute:plantuml (body params)
"Execute a block of plantuml code with org-babel.
diff --git a/testing/lisp/test-ob-plantuml.el b/testing/lisp/test-ob-plantuml.el
index b45d38be6..606b3a1f2 100644
--- a/testing/lisp/test-ob-plantuml.el
+++ b/testing/lisp/test-ob-plantuml.el
@@ -45,6 +45,36 @@ class CLASSNAME
(car src-block-info)
(car (cdr src-block-info)))))))))
+(ert-deftest
test-ob-plantuml/single-var-on-body-with-start-end-keywords-and-content-outside-the-keywords
()
+ "Test file output with input variable on BODY with @startxxx ... @endxxx
keywords
+and pre-body / post-body content."
+ (should
+ (string=
+ "'pre-body content
+@startuml
+!define CLASSNAME test_class
+class CLASSNAME
+@enduml
+'post-body content"
+ (let ((org-plantuml-jar-path nil))
+ (org-test-with-temp-text
+ "#+name: variable_value
+: test_class
+
+#+header: :file tmp.puml
+#+header: :var CLASSNAME=variable_value
+#+begin_src plantuml
+'pre-body content
+@startuml
+class CLASSNAME
+@enduml
+'post-body content
+#+end_src"
+ (org-babel-next-src-block)
+ (let ((src-block-info (cdr (org-babel-get-src-block-info))))
+ (org-babel-plantuml-make-body
+ (car src-block-info)
+ (car (cdr src-block-info)))))))))
(ert-deftest test-ob-plantuml/prologue ()
"Test file output with prologue."
--
2.51.0