Find/replace in each paragraph of the selection

Get help using and writing Nisus Writer Pro macros.
Post Reply
johseb
Posts: 47
Joined: 2016-02-13 10:01:29

Find/replace in each paragraph of the selection

Post by johseb »

A document of mine contains some text like the following (there's a tab between the text and the number; apparently forum sw doesn't allow it):

Some text 12
Other text 43
Some more text 62
etc

I would like to select all that lines and run a macro that adds a prefix and suffix to each line—two fixed strings:

Pre Some text 12 Post
Pre Other text 43 Post
Pre Some more text 62 Post
etc

That's what I cobbled together so far:

Code: Select all

$doc = Document.active
if $doc == undefined
	exit  # exit silently
end

$selection = $doc.selection
$lines = $selection.subtext.find '^.+\n','Ea'

foreach $line in $lines
	$myNewLine = $line.subtext.findAndReplace '^.+?\t\d+', 'Pre ' & '\0' & ' Post', 'E'
	$selection.subtext.replaceInRange $line.range, $myNewLine
End
The macro seems to loop through the lines and find/replace lines' text but I don't know how to actually modify the text in the document.

Any help with this trivial problem would be greatly appreciated!
User avatar
phspaelti
Posts: 1313
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Find/replace in each paragraph of the selection

Post by phspaelti »

Hello JohSeb,
Let me first ask, is there a particular reason why you are trying to do this with a macro? You can accomplish exactly this with Find/Replace directly, and you can also just macroize the Find/Replace expression. That should look something like this:

Code: Select all

Find And Replace '^.+?\t\d+',"Pre \\0 Post", 'Ea'
The reason why your macro doesn't produce any result is that you are operating on ".subtext"s. A subtext is a temporary copy of a part of a document. Your macro will affect these copies and then throw them away without affecting the main document.

There are quite a few other issues with your macro as currently written, so in a way it's good you are working on the subtext's, otherwise your macro might be making quite a mess of your file :D
philip
johseb
Posts: 47
Joined: 2016-02-13 10:01:29

Re: Find/replace in each paragraph of the selection

Post by johseb »

Hello Philip,
thanks for your reply.

The code I posted is just the first step in building a macro that should perform a replace with a variable string based on user input (i.e. the macro should check user input against the number at the end of each line).

For the time being it would be enough to undertsand the basic find/replace in selection.
Don't mean to be elusive or misterious, just want to try and learn NWP macro syntax.
I suspected the problem with my macro was that I'm not acting on the actual text of the document but I can't seen to be able to find a way to modify the text.
You also mentioned other issues in the code; could you be so kind as to show me the correct way to perform such a find/replace?

Thanks again.
User avatar
phspaelti
Posts: 1313
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Find/replace in each paragraph of the selection

Post by phspaelti »

Okay then.
First, if you want to allow for variable pre/post you can do this like this:

Code: Select all

$pre = '<pre>'
$post = '<post>'
Find And Replace '^.+?\t\d+', $pre &'\0' & $post, 'Ea'
Now let's consider how we can do find/replace in the macro language. Find and Replace is an operation on text, so you need a text object. For the document as a whole this would be:

Code: Select all

$doc.text.findAndReplace '^.+?\t\d+', $pre &'\0' & $post, 'Ea'
So what is the difference?
The most obvious difference is if you catch the result in a variable ($result = …), then the command style will result in an integer matching the number of replacements, while the macro command will return an array of text selection objects.

Now you want to do some more complex calculation on the string before doing the replace. In that case you can do the find/replace in two steps. Since the macro command returns a text selection object we can use that to do the replace:

Code: Select all

$result = $doc.text.find '^.+?\t\d+', 'E'
$result.text.replaceInRange $result.range, $pre & $result.subtext & $post
Since $result is a text selection object it contains all the bits we need: $result.text is the original text object where we did the find, the $result.range is the location where we want to replace, and $result.subtext is a copy of the part of the main text that matches the find expression.

Now note that in this example I did this for only one instance. If you want to do this for all instances you will need to put this in a loop as in your original code, but there is an important point to note. The code works with ranges. And ranges are simple numbers that count from the beginning of the file. If you change the file, then any range after the change is likely to be 'off'. So to avoid any problems you will want to work from back-to-front. So the loop for the above code becomes:

Code: Select all

$results = $doc.text.find '^.+?\t\d+', 'Ea'
foreach $result in reversed $results
$result.text.replaceInRange $result.range, $pre & $result.subtext & $post
end
Here I changed the options to 'Ea' to find all, and then I catch the $results (plural). Then I loop through them in reverse.

So that's the basic approach.
philip
User avatar
phspaelti
Posts: 1313
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Find/replace in each paragraph of the selection

Post by phspaelti »

Now for the second point:
johseb wrote: 2019-09-08 15:12:06… the macro should check user input against the number at the end of each line).
In the previous code, the "found bit" is in the $result.subtext. Since you will want the number to check against, how to do that?

The trick is another clever feature of the macro language find. As you are probably aware you can use parentheses to match bits of the find expression. Since $result.subtext contains a bit of text followed by a tab and a number, and subtext is a text object, we can find the bits we want like this:

Code: Select all

$result.subtext.find '^(.+)\t(\d+)', 'E'
and if we wanted to refer to those found bits in the replace, we could use '\1' for the text and '\2' for the number. But our replace is now a .replaceInRange command and so we can't do that.

What we can do here is use the '¢' option. This allows us to save the found bits in variables:

Code: Select all

$result.subtext.find '^(.+)\t(\d+)', 'E¢'
if $2 == 43
    $pre = 'special pre'
    $post = 'special post'
end
$result.text.replaceInRange $result.range, $pre & $0 & $post
Note that you can now also refer to the whole found bit with the variable name $0. It's even possible to give cleverer names to any found bits (rather than $1, $2, etc.).

Anyhow hopefully this should give you the bits you need.
philip
johseb
Posts: 47
Joined: 2016-02-13 10:01:29

Re: Find/replace in each paragraph of the selection

Post by johseb »

Hi Philip,
thanks for the thorough expalanation and step-by-step construction of the macro; really helpful (especially the tip about processign the array in reverse order).

I have a couple of remarks:

1. The macro acts on the entire file and modify all bits of text matching the regex. I would like to select a few lines and act on the selected text only. I can I get the Text object of the selection as opposed to the entire file?

2. The injected strings (Pre and Post) are formatted text i.e. they show the formatting of the .nwm macro file. How can I add plain text so that the added strings inherit the formatting of the destination document?
User avatar
phspaelti
Posts: 1313
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Find/replace in each paragraph of the selection

Post by phspaelti »

Ah, yes, these are good questions.

1. Find in Selection
The macro command .find can unfortunately not be limited to a selection, the way the menu command can. There are a couple of ways to work around this.

Method 1: You can do the initial find using the menu command "Find in Selection" and then use $doc.selections to retrieve the results.

Method 2: You can do the macro .find command on the $selection.subtext (the way you originally wanted). However now you have to make sure to go back to the original text object to do the changes, but also remember that the .range objects of your find results will all have to be offset by the amount of the selection. This means you have to calculate the ranges first. So things would look like this:

Code: Select all

$sel = $doc.selection
$results = $sel.subtext.find '^.+?\t\d+', 'Ea'
foreach $result in reversed $results
    $range = Range.new $sel.location + $result.location, $result.length
    $doc.text.replaceInRange $range, $pre & $result.subtext & $post
end
I personally rather dislike this approach, and try to avoid it.

Method 3: You can do the find on the whole document and just throw away the ones you don't need. Basically test the $result.range to see whether it is in the $sel.range. So something like this:

Code: Select all

$sel = $doc.selection
$results = $doc.text.find '^.+?\t\d+', 'Ea'
foreach $result in reversed $results
    if $sel.range.contains $result.range
        $result.text.replaceInRange $result.range, $pre & $result.subtext & $post
    end
end
It should be noted that Method 1 should work even for non-contiguous selections, while 2 and 3 would require more work in that case.

2. Text formatting of variables
This is pretty standard pain in the *. There should be some explanations from Martin somewhere about how these things are decided. Maybe using @String literals might solve your problem. It may also be because you are inserting the string at the beginning of the paragraph. I'd have to test this myself first.
philip
johseb
Posts: 47
Joined: 2016-02-13 10:01:29

Re: Find/replace in each paragraph of the selection

Post by johseb »

Hi Philip,
I'm making slow progresses thanks to your help.

1. Find in Selection
- Method 1. I'm afraid I don't understand your hint. Could you please give a worked out example?

- Method 2: I agree with you it's a bit convoluted

- Method 3: that worked perfectly so it's presently my go-to method. I just needed to modify a bit the syntax since the Selection object doesn't have a .range property:

Code: Select all

$sel = $doc.selection
$results = $doc.text.find '^.+?\t\d+', 'Ea'

foreach $result in reversed $results
	if $sel.textSelection.range.containsLocation $result.location
		$resultText = $result.subtext        
		$result.text.replaceInRange $result.range, $pre & $result.subtext & $post
    end
		
End
2. Text formatting of variables
You're right; using a plain text @string literals to declare the substitution text works

Code: Select all

$pre = @String<Prefix text >
Out of curiosity, what if I'm not defining my text strings but I'm getting from an existing formatted text (e.g. some text excerpt found in the document)? Is it possible to strip the formatting away?
User avatar
phspaelti
Posts: 1313
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Find/replace in each paragraph of the selection

Post by phspaelti »

johseb wrote: 2019-09-09 06:19:071. Find in Selection
- Method 1. I'm afraid I don't understand your hint. Could you please give a worked out example?

Code: Select all

Find '^.+?\t\d+', 'Eas'
$results = $doc.textSelections
…
johseb wrote: 2019-09-09 06:19:07 … I just needed to modify a bit the syntax since the Selection object doesn't have a .range property:
Sorry about that. Actually my usual approach would be to directly grab the text selection (rather than the selection):

Code: Select all

$sel = $doc.textSelection
…
I didn't realize that the selection object doesn't have a .range property. Learned something new about NW macros :D
johseb wrote: 2019-09-09 06:19:07 Out of curiosity, what if I'm not defining my text strings but I'm getting from an existing formatted text (e.g. some text excerpt found in the document)? Is it possible to strip the formatting away?
Sure. The most direct is to use an explicit type cast:

Code: Select all

$string = Cast to String $text
There may be other ways as well, depending on the situation, Also note that selections have a .substring property in addition to the .subtext. It might be more efficient to work with strings generally, unless you need (or want) to keep the attributes.
philip
johseb
Posts: 47
Joined: 2016-02-13 10:01:29

Re: Find/replace in each paragraph of the selection

Post by johseb »

Philip,
thanks again for all your detailed and clear explanations!

I'm sure they'll be useful for other beginners taking their first steps in NWP macro syntax.
User avatar
martin
Official Nisus Person
Posts: 5228
Joined: 2002-07-11 17:14:10
Location: San Diego, CA
Contact:

Re: Find/replace in each paragraph of the selection

Post by martin »

Thanks to Philip for all the great explanations and suggestions! It seems everything is wrapped up here but I just wanted to add a few small replies:
phspaelti wrote: 2019-09-09 04:47:09Method 1: You can do the initial find using the menu command "Find in Selection" and then use $doc.selections to retrieve the results.
This is the approach I'd use. It's much simpler than worrying about range intersections or which text object(s) you're working on. As Philip showed it's simple to search in the current selection using the Find commands, and then inspect the Document object to get the matching TextSelection objects.
I didn't realize that the selection object doesn't have a .range property
It's true there's no .range property for a generic Selection object. Some selections aren't bound to indexes in the text (eg: floating shape selections). But generic Selection objects also do not have a .location property. The code accessing the .location worked because the selections were actually TextSelection objects. That's guaranteed when using the .find command, as matches are always made in text.

That said, even when using things like "Document.selection" you might get away with using TextSelection properties like .range. Even though the value returned is declared to be a Selection object, it might really be a TextSelection "under the hood", depending on the situation, and thus behave as such. Of course properly written macro code shouldn't rely on this. If your code needs to work with TextSelection properties then you should always obtain one through a command declared to return a TextSelection.
Post Reply