Applying style to paragraphs below found text until next style

Get help using and writing Nisus Writer Pro macros.
Post Reply
User avatar
ScottinPollock
Posts: 24
Joined: 2017-09-11 08:16:47

Applying style to paragraphs below found text until next style

Post by ScottinPollock » 2018-07-18 13:16:17

Hi everybody...

So I now need to grok the Macro thing in order to do some long document formatting, and I must admit I am finding it a little obtuse (at least compared to the Classic Mac days of Nisus).

I want to find each occurence of the string "Result Codes" in the document;

then change the style of the following line to a paragraph style "Result Codes";

continue doing this with each following line until the I run into a line with paragraph style "Header 2"

Any help greatly appreciated!

-SiP

User avatar
phspaelti
Posts: 941
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Applying style to paragraphs below found text until next style

Post by phspaelti » 2018-07-18 21:42:20

Hello Scott,
let me see if I understand what you are trying to do. Your document has the structure:

Heading 2
stuff
Result codes
result
result
Heading 2
stuff


And I'm guessing that there is at most one paragraph with the string "Result Codes" per Heading-2 section. You want to apply a style to the results which form the tail end of each "Heading 2" section.

Before worrying about the macro language, you'll really need to find a suitable algorithm. There are basically two approaches.

First, a procedural "Turing machine" style approach would be to check each paragraph in the document. If the document contains the string "Result Codes" kick into second gear, and keep applying the style until you reach a "Heading 2"-styled paragraph. Then go back into first gear.

Second, a more holistic approach is to find all "Result Code" strings and all "Heading 2" paragraphs, and then match them up in pairs to find the stretches that contain results.

I'll add a reply for each of the two approaches.
philip

User avatar
phspaelti
Posts: 941
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Applying style to paragraphs below found text until next style

Post by phspaelti » 2018-07-18 21:59:56

So let's try the procedural approach.

In this approach you could apply the style 'as you go', but I recommend against it. Instead you should create a selection that selects all the result sections at the same time. Then you can apply style as you want. If your document has any kind of size this will almost certainly be faster. In the code below, I'm only going to solve the selection problem.

So first we need all the paragraphs so we can go through them one-by-one. There are several ways to do this, but the easiest is to use Find. In NWML this will look like this:

Code: Select all

$doc = Document.active
$paras = $doc.text.find('^.+\n','Ea')
To loop through all the paras we do:

Code: Select all

foreach $para in $paras
    # do stuff
end
To represent the two working states (the 'gear') we can use a boolean variable:

Code: Select all

$gear2 = @false
…
if $gear2
    # now we're in gear 2
else
    # now we're back in gear 1
end
To check if the current paragraph contains the string "Result Codes" you can use the following:

Code: Select all

if $para.substring.find('Result Codes')
And to check if the paragraph has the style "Heading 2" you will need to get the text attributes and check them:

Code: Select all

$attr = $para.text.attributesAtIndex $para.location
if $attr.paragraphStyleName == 'Heading 2'
My completed code looks like this:

Code: Select all

$doc = Document.active

$gear2 = @false
$resultSels = Array.new
foreach $para in $doc.text.find('^.+\n','Ea')
  if ! $gear2
    if $para.substring.find('Result Codes')
      $gear2 = @true
    end
  else
    $attr = $para.text.attributesAtIndex $para.location
    if $attr.paragraphStyleName == 'Heading 2'
      $gear2 = @false
    else
      $resultSels.push $para
    end
  end
end

$doc.setSelection $resultSels
Last edited by phspaelti on 2018-07-18 23:00:59, edited 1 time in total.
philip

User avatar
phspaelti
Posts: 941
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Applying style to paragraphs below found text until next style

Post by phspaelti » 2018-07-18 22:43:09

Ok, and now for the 'holistic' version.
This time we can just find the stuff we are actually looking for:

Code: Select all

$doc = Document.active
$text = $doc.text
$h2Style = $doc.styleWithName 'Heading 2'
$resultCodeSels = $text.findAll '^.*Result Codes.*\n', 'Ea'
$h2Sels = $text.findAll $h2Style
So this gives us two batches of text selections. The part we want to select is from end of any of the 'Result Code' selections to the beginning of the next "Heading 2" selection. So the 'Result Code' selections will be the driver of the loop.

There is one little sticking point. Presumably the last "Heading 2" section will not have a "Heading 2" paragraph as the delimiter (at the end). This means that we could run out of "Heading 2" selections. The easiest way to fix this is to add a selection that will always be at the end. That of course would be the end of the document. So I'll add an end of the document selection to the array of "Heading 2" selections. This can be done like this:

Code: Select all

$endOfDocSel = TextSelection.new $text, Range.new($text.length, 0)
$h2Sels.push $endOfDocSel
So now the matching up procedure. The "Heading 2" selections are in document order. So we'll take the first one from the queue. If this precedes our current "Result Codes" selection, then we need to get the next one. Finally when we have a match, we create the desired selection from the end (the bound) of the "Result Codes" selection to the beginning (the location) of the "Heading 2" selection.

Code: Select all

$resultSels = Array.new
$nextH2Sel = $h2Sels.dequeue
foreach $sel in $resultCodeSels
  while $nextH2Sel.location < $sel.location
    $nextH2Sel = $h2Sels.dequeue
  end
  $resultSels.push TextSelection.newWithLocationAndBound($text, $sel.bound, $nextH2Sel.location)
end
Completed code is as follows:

Code: Select all

$doc = Document.active
$text = $doc.text
$h2Style = $doc.styleWithName 'Heading 2'

$resultCodeSels = $text.findAll '^.*Result Codes.*\n', 'Ea'
$h2Sels = $text.findAll $h2Style
$h2Sels.push TextSelection.new($text,Range.new($text.length,0))

$resultSels = Array.new
$nextH2Sel = $h2Sels.dequeue
foreach $sel in $resultCodeSels
  while $nextH2Sel.location < $sel.location
    $nextH2Sel = $h2Sels.dequeue
  end
  $resultSels.push TextSelection.newWithLocationAndBound($text, $sel.bound, $nextH2Sel.location)
end

$doc.setSelection $resultSels
philip

User avatar
ScottinPollock
Posts: 24
Joined: 2017-09-11 08:16:47

Re: Applying style to paragraphs below found text until next style

Post by ScottinPollock » 2018-07-19 06:22:33

Philip...

First I want to thank you for your time and expertise!
phspaelti wrote:
2018-07-18 21:59:56
So let's try the procedural approach.
Good... as it is the most readable (to me), even though the second method may be more efficient.

Code: Select all

$doc = Document.active
$paras = $doc.text.find('^.+\n','Ea')
It appears this is one very granular language. I am really surprised there is no simple function or property to return the lines of a document, but I see what your doing. It actually is putting range descriptors in the array.

Code: Select all

$gear2 = @false
$resultSels = Array.new
foreach $para in $doc.text.find('^.+\n','Ea')
  if ! $gear2
    if $para.substring.find('Result Codes')
      $gear2 = @true
    end
  else
    $attr = $para.text.attributesAtIndex $para.location
    if $attr.paragraphStyleName == 'Heading 2'
      $gear2 = @false
    else
      $resultSels.push $para
    end
  end
end
This logic certainly works and is understood. However it was not what I set out to do which was:

Code: Select all

loop through the doc looking for "Result Codes"
  get the line number
  then a nested loop to
    add 1 to it
    exit loop if the style of line it was "Header 2"
    set the style of line it to "result codes"
  end loop
end loop
So thank you again, you have taught me much about selections in NWPM, but one (possibly stupid) question remains: Your code makes the selections, and I can easily click on the style in the Tool Drawer to apply it, but how do I apply it in the macro? I see the .apply command, but have been through the manual twice and haven't come up with a clue on how to apply it.

User avatar
phspaelti
Posts: 941
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Applying style to paragraphs below found text until next style

Post by phspaelti » 2018-07-19 07:11:48

ScottinPollock wrote:
2018-07-19 06:22:33
It appears this is one very granular language. I am really surprised there is no simple function or property to return the lines of a document, but I see what your doing. It actually is putting range descriptors in the array.
I guess you're right in the sense that Nisus does not have any direct access to 'lines' (= paragraphs). Location in text is handled by indexes and ranges. There is a text command to get the range of a paragraph for any given location. But using that to retrieve paragraphs requires looping through the document as well.
But ultimately it's probably best to get away from the idea that files are processed line by line, and instead use Find (≈grep) which can directly return the ranges you need. If you prefer line-by-line processing you could always use Perl, but that doesn't work if you want to use styles.
So thank you again, you have taught me much about selections in NWPM, but one (possibly stupid) question remains: Your code makes the selections, and I can easily click on the style in the Tool Drawer to apply it, but how do I apply it in the macro? I see the .apply command, but have been through the manual twice and haven't come up with a clue on how to apply it.
Well there is still the simplest way which was already possible back in Nisus Classic: just write the name of the menu command on a line :D
So assuming you have a style called "Result Style", you can write:

Code: Select all

Paragraph Style:Result Style
and the style will be applied to the current selection.

If you want to do this in macro language you have to first get the Style object. So that looks like this:

Code: Select all

$style = $doc.styleWithName 'Result Style'
$style.apply
If you don't need the style for anything else and don't want to 'waste' a variable for it you can write this as one line like this:

Code: Select all

$doc.styleWithName('Result Style').apply
As with most constructs in the language you will need the document object to do these things, so it's probably best to get in the habit of putting a $doc = … command at the beginning of all your macros.
philip

User avatar
ScottinPollock
Posts: 24
Joined: 2017-09-11 08:16:47

Re: Applying style to paragraphs below found text until next style

Post by ScottinPollock » 2018-07-19 07:34:42

phspaelti wrote:
2018-07-19 07:11:48
If you don't need the style for anything else and don't want to 'waste' a variable for it...
Can I safely assume variables are released when the macro is finished?

And regarding the object model in play here, it would be nice to write that data to files on disk (to aid my learning curve here). As with:

Code: Select all

$doc.text.find('^.+\n','Ea')
I assume this creates an array of range objects. I can inspect them using 'prompt' (well, at least some of them), but when I write that data to a file:

Code: Select all

$thePranges = $doc.text.find('^.+\n','Ea')
File.writeDataToPath $thePranges,"/Users/ss/Desktop/ranges.txt"
I get a file with 4 bytes in it. Is there a way to convert the object contents to plain text?

Thanks again for all your help... this is a lot of fun and will take NWP to a whole new level for me.

User avatar
phspaelti
Posts: 941
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Applying style to paragraphs below found text until next style

Post by phspaelti » 2018-07-19 08:09:04

Addendum:
I just remembered, Nisus does have a command to access paragraphs by number:

Code: Select all

Select Paragraph 5
I rarely ever use or even look at those selection commands, but they do exist. The main problem with addressing paragraphs by number is that there is no real way to know the number of any given paragraph, e.g., the paragraph of the current selection. But there are also selection commands for Select Next Paragraph, Select Previous Paragraph and Select Relative Paragraph (using an offset).
So your original idea could be implemented like this:

Code: Select all

Select Document Start
While Find 'Result Codes', '-W'
  while Select Next Paragraph
    $sel = TextSelection.active
    $attr = $sel.text.attributesAtIndex $sel.location
    if $attr.paragraphStyleName == 'Heading 2'
      break
    end
    Paragraph Style:Result Style
  end
end
philip

User avatar
ScottinPollock
Posts: 24
Joined: 2017-09-11 08:16:47

Re: Applying style to paragraphs below found text until next style

Post by ScottinPollock » 2018-07-19 10:13:47

Think I am seeing a bug here... not in your Macro but in NWP.

I realized I had some text after the result codes with a 'Heading 1' style as well. Each one was after an inserted page break. No problem I figure... so in your 'as you go' method I just changed:

Code: Select all

if $attr.paragraphStyleName == "Heading 2"
  $gear2 = @false
else
  $resultSels.push $para
end
to:

Code: Select all

if $attr.paragraphStyleName == "Heading 2"
  $gear2 = @false
elseIf $attr.paragraphStyleName == "Heading 1"
  $gear2 = @false
else
  $resultSels.push $para
end
It missed every "Heading 1" after the page breaks. In fact, those paragraphs were not in the array created by $doc.text.find('^.+\n','Ea'). It wasn't until I put a CR in front of those header 1 paragraphs (after the page breaks) that text.find actually included them in the array. Weird! BTW I could easily reproduce this behavior in a new document.

On the other hand... The following works perfectly (although much slower):

Code: Select all

Select Document Start
While Find 'Result Codes', '-W'
  while Select Next Paragraph
    $sel = TextSelection.active
    $attr = $sel.text.attributesAtIndex $sel.location
    if $attr.paragraphStyleName == 'Heading 2'
      break
    elseIf $attr.paragraphStyleName == 'Heading 1'
      break
    end
    Paragraph Style:result codes it
  end
end

User avatar
phspaelti
Posts: 941
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Applying style to paragraphs below found text until next style

Post by phspaelti » 2018-07-19 19:05:10

ScottinPollock wrote:
2018-07-19 10:13:47
Think I am seeing a bug here... not in your Macro but in NWP.
I think this is a "feature", not a bug :wink: Page breaks are treated as text characters, and are not counted as start of paragraphs, though they allow different paragraph styles on either side. It's a bit weird, but, I believe, a result of Nisus' legacy treatment of page breaks.
There even seems to be some confusion on Nisus Co.'s part about this, as the Find Wildcard "Any" is (?:.|\n|\f)+ where the '\f' seems to be superfluous.

But…
When you write:
It missed every "Heading 1" after the page breaks. In fact, those paragraphs were not in the array created by $doc.text.find('^.+\n','Ea'). It wasn't until I put a CR in front of those header 1 paragraphs (after the page breaks) that text.find actually included them in the array.
Did you not have CRs before the page breaks? (That is where I would put them.)

So if you are in the habit of using page breaks without CRs you might want to use the find expression '(?<=^|\f)[^\f]+(?:\n|\f|$)' instead to locate your 'lines'.
I realized I had some text after the result codes with a 'Heading 1' style as well. Each one was after an inserted page break. No problem I figure... so in your 'as you go' method I just changed:

Code: Select all

if $attr.paragraphStyleName == "Heading 2"
  $gear2 = @false
else
  $resultSels.push $para
end
to:
[snip]
I would suggest the following instead:

Code: Select all

if $attr.paragraphStyleName.find("Heading \d",'E')
  $gear2 = @false
else
  $resultSels.push $para
end
That will cover all Heading styles. But of course it will also not work, if you have page breaks inside paragraphs. It will still depend on using the more complicated find string for "paragraphs"
On the other hand... The following works perfectly (although much slower):

Code: Select all

Select Document Start
While Find 'Result Codes', '-W'
  while Select Next Paragraph
    $sel = TextSelection.active
    $attr = $sel.text.attributesAtIndex $sel.location
    if $attr.paragraphStyleName == 'Heading 2'
      break
    elseIf $attr.paragraphStyleName == 'Heading 1'
      break
    end
    Paragraph Style:result codes it
  end
end
NB: In this code too, you can use the .find('Heading') condition instead.

You're right. The Select Next Paragraph command treats text terminated by a PB as a paragraph. The above code is slow, because it does all the work via the visual interface (the "light show" of Classic days). It just doesn't look as flashy anymore, since computers are faster (and Nisus probably has some optimizations to suppress redrawing, or something). So in the end I would really recommend the 'holistic' method, though that too would require some adjustments for handling multiple Heading styles.

But just to summarize the break character ('\f') is treated as a text character in grep. So it's not a bug, and it isn't something Nisus can change.
philip

User avatar
phspaelti
Posts: 941
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Applying style to paragraphs below found text until next style

Post by phspaelti » 2018-07-20 00:44:58

phspaelti wrote:
2018-07-19 19:05:10
So in the end I would really recommend the 'holistic' method, though that too would require some adjustments for handling multiple Heading styles.
Anyhow here is the macro for that. The heading paragraph text selections are gathered one style at a time for each of the heading styles, and then all combined together. But since we need them in document order, the "trick" is to sort them.

Code: Select all

$doc = Document.active
$text = $doc.text

# Get all paragraphs containing the phrase 'Result Codes'
$resultCodeSels = $text.findAll '^.*Result Codes.*\n', 'Ea'

# Get all Heading paragraphs
$hSels = Array.new
foreach $style in $doc.paragraphStyles
  if $style.name.find('Heading')
    $hSels.appendValuesFromArray $text.findAll($style)
  end
end
# Make sure they are in document order
$hSels.sort
# Add an end-of-doc delimiter
$hSels.push TextSelection.new($text,Range.new($text.length,0))

# Match each 'Result Codes' paragraph to a following heading
$resultSels = Array.new
$nextHSel = $hSels.dequeue
foreach $sel in $resultCodeSels
  while $nextHSel.location < $sel.bound
    $nextHSel = $hSels.dequeue
  end
  # Add a text selection from the end of the 'Result Codes' to the start of the next heading
  $resultSels.push TextSelection.newWithLocationAndBound($text, $sel.bound, $nextHSel.location)
end

# Select all the bits
$doc.setSelection $resultSels
Last edited by phspaelti on 2018-07-20 18:48:14, edited 1 time in total.
philip

User avatar
ScottinPollock
Posts: 24
Joined: 2017-09-11 08:16:47

Re: Applying style to paragraphs below found text until next style

Post by ScottinPollock » 2018-07-20 09:48:17

phspaelti wrote:
2018-07-19 19:05:10
When you write:
It missed every "Heading 1" after the page breaks. In fact, those paragraphs were not in the array created by $doc.text.find('^.+\n','Ea'). It wasn't until I put a CR in front of those header 1 paragraphs (after the page breaks) that text.find actually included them in the array.
Did you not have CRs before the page breaks? (That is where I would put them.)
I insert the page breaks with the insertion point at the end of the line of the paragraph previous to the one that is to be the heading on the next page. Oddly enough, this inserts a blank paragraph above the heading on the next page (which I remove with a forward delete). This method shows a paragraph formatting icon next to the heading, where inserting the page break with the insertion point at the left of the heading text does not. I had assumed that if the paragraph formatting icon was displayed, I had a real paragraph there.

So while this may not technically be a bug, I find it pretty unexpected behavior. In a simple two page document with a page break, the only way $doc.text.find('^.+\n','Ea') will return ranges for all paragraphs with text is to include an empty CR before the PB, and another empty CR after the PB. There is just no way I can format my documents that way.
So if you are in the habit of using page breaks without CRs you might want to use the find expression '(?<=^|\f)[^\f]+(?:\n|\f|$)' instead to locate your 'lines'.
That throws an unexpected error when used in the form of $doc.text.find(?<=^|\f)[^\f]+(?:\n|\f|$). Obviously I am missing something.
So in the end I would really recommend the 'holistic' method, though that too would require some adjustments for handling multiple Heading styles.

Anyhow here is the macro for that. The heading paragraph text selections are gathered one style at a time for each of the heading styles, and then all combined together. But since we need them in document order, the "trick" is to sort them.
This aslo throws an error here on line 27 "The given bound (51492) is less than the range location (51505)."

User avatar
phspaelti
Posts: 941
Joined: 2007-02-07 00:58:12
Location: Japan

Re: Applying style to paragraphs below found text until next style

Post by phspaelti » 2018-07-20 18:11:08

ScottinPollock wrote:
2018-07-20 09:48:17
I insert the page breaks with the insertion point at the end of the line of the paragraph previous to the one that is to be the heading on the next page. Oddly enough, this inserts a blank paragraph above the heading on the next page (which I remove with a forward delete). This method shows a paragraph formatting icon next to the heading, where inserting the page break with the insertion point at the left of the heading text does not. I had assumed that if the paragraph formatting icon was displayed, I had a real paragraph there.
What it seems you are doing is inserting the PB before the CR. Nisus is not 'inserting' a CR, it's just moving it to the next line. When you delete it, you end up with a single paragraph broken across two pages. Nisus, reasonably, but confusingly, allows you to apply separate paragraph styles to the two halves of the paragraph, and "Select Next Paragraph" treats them separately, but Find/grep does not.
So while this may not technically be a bug, I find it pretty unexpected behavior. In a simple two page document with a page break, the only way $doc.text.find('^.+\n','Ea') will return ranges for all paragraphs with text is to include an empty CR before the PB, …
That's what I would recommend…
…and another empty CR after the PB.
I don't see why you would need to do that. I would only use a CR after a PB if I needed extra space there (since Nisus 'gobbles up' the Space Before at the top of the page).
Don't get me wrong, I don't think this is great. This is simply the result of using ASCII 12 ("Form Feed") for PB, and it's a place where Nisus' way of doing things feels very 'legacy'. Already in the Classic days I remember having discussions about replacing this with CRs that can enforce a page break. Maybe someday…
So if you are in the habit of using page breaks without CRs you might want to use the find expression '(?<=^|\f)[^\f]+(?:\n|\f|$)' instead to locate your 'lines'.
That throws an unexpected error when used in the form of $doc.text.find(?<=^|\f)[^\f]+(?:\n|\f|$). Obviously I am missing something.
Sorry if I was a bit unclear. The find expression will need to include the whole thing, including the single quotes. You can put this after the .find with or without parentheses:

Code: Select all

$doc.text.find '(?<=^|\f)[^\f]+(?:\n|\f|$)', 'Ea'
or

Code: Select all

$doc.text.find('(?<=^|\f)[^\f]+(?:\n|\f|$)', 'Ea')
So in the end I would really recommend the 'holistic' method, though that too would require some adjustments for handling multiple Heading styles.

Anyhow here is the macro for that. The heading paragraph text selections are gathered one style at a time for each of the heading styles, and then all combined together. But since we need them in document order, the "trick" is to sort them.
This aslo throws an error here on line 27 "The given bound (51492) is less than the range location (51505)."
Clearly, there is something I am not understanding about the structure of your document. Apparently there is a place where the 'Code Results' paragraph and the 'Heading' style overlap. (By a total of 13 characters. I'm guessing that the "Results Code" paragraph has a Heading style applied.) You can get the macro to complete without error by changing the following code line:

Code: Select all

while $nextHSel.location < $sel.location
to

Code: Select all

while $nextHSel.location < $sel.bound
Which is really the way I should have written it in the first place. So I'm fixing the line in the earlier code. :wink:
philip

User avatar
ScottinPollock
Posts: 24
Joined: 2017-09-11 08:16:47

Re: Applying style to paragraphs below found text until next style

Post by ScottinPollock » 2018-07-21 03:56:31

phspaelti wrote:
2018-07-20 18:11:08
What it seems you are doing is inserting the PB before the CR. Nisus is not 'inserting' a CR, it's just moving it to the next line.
Got it. So you're suggesting the PB be on its own line. Unfortunately I can't always do that (that extra CR can sometimes force it to the next page). It'd be nice to have a PB that didn't actually require any vertical space in the document.
Sorry if I was a bit unclear. The find expression will need to include the whole thing, including the single quotes. You can put this after the .find with or without parentheses:

Code: Select all

$doc.text.find '(?<=^|\f)[^\f]+(?:\n|\f|$)', 'Ea'
or

Code: Select all

$doc.text.find('(?<=^|\f)[^\f]+(?:\n|\f|$)', 'Ea')
Got it. Wasn't positive I needed the Ea (pretty sure I did), but didn't know how to pass it with this find string cause of two sets of parens... seems passing options is pretty flexible.
Clearly, there is something I am not understanding about the structure of your document. Apparently there is a place where the 'Code Results' paragraph and the 'Heading' style overlap. (By a total of 13 characters. I'm guessing that the "Results Code" paragraph has a Heading style applied.)
Yup. The line with the string "Result Codes" has a style of "Heading 3".
You can get the macro to complete without error by changing the following code
Thanks! It would be nice if the Macro guide had a few more real world syntax templates, but I think I have enough to cobble things together for any future needs. I don't need this kind of thing often, but with hundreds of pages documenting these routines that did not have that particular style applied, this was a real time saver (and a long overdue exercise).

Thanks again for your help on this!

-SiP

Post Reply