Links that are both local and cross-deliverable in shared topics


Chris Papademetrious
 

We have two books that share a common topic, called "Shared Topic":

Book A:
  • Topic 1
  • Topic 2
  • Shared Topic
Book B:
  • Topic 3
  • Topic 4
  • Shared Topic
In "Shared Topic", we want to include a cross-reference to "Topic 1", such that it is a local link in Book A and a cross-book link in Book B.

Each book has the other book defined as a peer book, with mapref-level keyscopes:

<bookmap>
  <title>Book A</title>
  <mapref href="bookB.ditamap" ... scope="peer" keyscope="B"/>
  ...
</bookmap>

<bookmap>
  <title>Book B</title>
  <mapref href="bookA.ditamap" ... scope="peer" keyscope="A"/>
  ...
</bookmap>

If I write <xref keyref="topic1"/>, the link works in book A but not book B.

If I write <xref keyref="A.topic1"/>, the link works in book B but not book A.

So I tried adding local (map-level) keyscopes to each book, which "2.3.4.2 Keyscopes" suggests would allow references to A.* to resolve locally:

<bookmap keyscope="A">
  <title>Book A</title>
  <mapref href="bookB.ditamap" ... scope="peer" keyscope="B"/>
  ...
</bookmap>

<bookmap keyscope="B">
  <title>Book B</title>
  <mapref href="bookA.ditamap" ... scope="peer" keyscope="A"/>
  ...
</bookmap>

Unfortunately, Oxygen doesn't create cross-book links correctly with this map configuration - it concatenates the mapref-level and map-level keyscopes (for example, B.B.topic1 instead of B.topic1) instead of treating them as a union as described in the spec.

Has anyone run into this situation before, or am I charting new territory here?

 - Chris

P.S. On the publishing side, we resolve cross-book links with this DITA-OT plugin. So mostly, I just need to solve the authoring and DITA-correctness aspects.


ekimber@contrext.com
 

DITA 1.3 introduced the ability to author links between two root maps, which is what you want here.

(You can find a small working sample here: https://github.com/dita-community/dita-test-cases/tree/master/cross-deliverable-links)

The basic technique is you define a mapref with a @scope value of "peer" that points to the root map of the target document (the document you are linking to) and a @keyscope value by which you can then refer to keys in the target root map.

So for your example, in the map for Book A you would give Topic 1 the "Topic_01".

Topics used in Book A can refer to Topic one by the unqualified key "Topic_01": <xref keyref="Topic_01"/>

In the map for Book B you would create this keydef to associate a key scope for Book A:

Book B:

<map>
...
<mapref keyscope="book_A" scope="peer" href="..\BookA\book_a.ditamap" format="ditamap"/>
...
</map>

Then in a topic used in Book B you can refer to topic 1 as:

<xref keyref="book_A.Topic_01"/>

Per the rules we defined for scope="peer" in DITA 1.3, this explicitly means "Go to the topic with the key 'Topic_01' as declared in the root map associated with the key scope 'book_A' in this root map."

If you need to be able to have topics that refer to Topic_01 also shared between books A and B, you can enable having all references to Topic_01 be the same in one of two different ways:

1. Use another level of indirection that goes from a global key ("Topic_01") to the appropriately-qualified key in the context of a given map:

Book A:

<topicref keys="Topic_01" href="topics/topic-01.dita"/>

Book B:

Resource-only topicref that points to topic-01 used from Book A:

<keydef keys="Topic_01" keyref="book_a.Subject_of_Topic_01"/>

With this approach, topics can blindly refer to key "Topic_01" without worrying about where the topic is used.

2. Put a key scope name on the Book A map so that a scope-qualified reference to key "book_a.Topic_01" will resolve in the context of Book A as well as in Book B:

Book A:

<map keyscope="book_a" ...>
...
</map>

An unqualified reference to key "Topic_01" from Book A will still work because the key and reference will be in the same scope.

Note that OxygenXML supports resolution of cross-deliverable links in the editor, so you can use it to validate your content as authored.

Cheers,

E.


--
Eliot Kimber
http://contrext.com


On 6/9/21, 11:57 AM, "Chris Papademetrious" <main@dita-users.groups.io on behalf of chrispitude@gmail.com> wrote:

We have two books that share a common topic, called "Shared Topic":

Book A:

* Topic 1
* Topic 2
* Shared Topic

Book B:

* Topic 3
* Topic 4
* Shared Topic

In "Shared Topic", we want to include a cross-reference to "Topic 1", such that it is a local link in Book A and a cross-book link in Book B.

Each book has the other book defined as a peer book, with mapref-level keyscopes:

<bookmap>
<title>Book A</title>
<mapref href="bookB.ditamap" ... scope="peer" keyscope="B"/>
...
</bookmap>


<bookmap>
<title>Book B</title>
<mapref href="bookA.ditamap" ... scope="peer" keyscope="A"/>
...
</bookmap>

If I write <xref keyref="topic1"/>, the link works in book A but not book B.

If I write <xref keyref="A.topic1"/>, the link works in book B but not book A.

So I tried adding local (map-level) keyscopes to each book, which "2.3.4.2 Keyscopes" <http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part2-tech-content/archSpec/base/keyScopes.html> suggests would allow references to A.* to resolve locally:

<bookmap keyscope="A">
<title>Book A</title>
<mapref href="bookB.ditamap" ... scope="peer" keyscope="B"/>
...
</bookmap>


<bookmap keyscope="B">
<title>Book B</title>
<mapref href="bookA.ditamap" ... scope="peer" keyscope="A"/>
...
</bookmap>

Unfortunately, Oxygen doesn't create cross-book links correctly with this map configuration - it concatenates the mapref-level and map-level keyscopes (for example, B.B.topic1 instead of B.topic1) instead of treating them as a union as described in the spec.

Has anyone run into this situation before, or am I charting new territory here?

- Chris

P.S. On the publishing side, we resolve cross-book links with this DITA-OT plugin <https://github.com/chrispy-snps/DITA-fix-xbook-html-links/>. So mostly, I just need to solve the authoring and DITA-correctness aspects.


Chris Papademetrious
 

Thanks Eliot! Excellent summary of the problem and the situation.

Your method #2 (adding a map-level @keyscope) is what I tried, but it doesn't seem to work in Oxygen. It doesn't form the cross-book links properly, and if I create them by hand, they don't resolve.

I will try method #1 tomorrow. However, I'm not sure how well it would work in practice, as the writers want to share entire chapters between books, with a variety of cross-references to both book within the chapters. Trying to create all the local/cross-book flavors of indirection links would be quite maddening!

The good news is, your reply and suggestion for method #2 gives me the confidence that this should be fixable in Oxygen. Thank you so much!

 - Chris


ekimber@contrext.com
 

Note that in method 1, if you set up the indirect keydefs for the topics that may be in other root maps, authors can blindly refer to the unqualified keys and it's up to map authors to ensure that the referenced keys are bound to either the same-publication use of the topics or other-publication uses, which you can do, for example, by having shared sets of keydefs that "export" the key names for the referenceable topics for each publication so they can be referenced without scope qualification. These keydef maps can easily be generated once you have the pattern set up.

So in your example, for Book A you could generate a map that provides the necessary mapdef to Book A's map and a set of indirection keydefs for use from Book B:

book_a/keydefs/book_a_cross-deliverable-keydefs.ditamap:

<map>
<title>Cross-Deliverable Keydefs for Book A</title>
<topicgroup>
<mapdef keyscope="book_a" scope="peer" href="../book_a.ditamap"/>
<keydef keys="topic_01" keyref="book_a.topic01"/>
...
</topicgroup>
</map>

Then in book_b.ditamap:

<map>
<mapref href="../book_a/keydefs/book_a_cross-deliverable-keydefs.ditamap"/>
...
</map>

The main challenge here is that unqualified keys need to be unique across all your publications, which usually suggests defining key names that reflect the subject of the topic rather than its structural position or source filename, so "install_framitz" rather than "topic_01" so that the keyname is both meaningful to authors and more sensibly unique across all topics regardless of use context.

Then in a topic you can simply have:

<p>Perform task <xref keyref="install_framitz"/> ...</p>

Without having to worry about whether the framitz installation topic is in the same deliverable or a different one.

Note also that it's *map authors* who manage the mapping of key names to uses of topics in the appropriate context.

Map authors have absolute power over key-to-resource bindings (because a root map can override any keydef in any included submap).

This fact suggests that if you've got the kind of re-use and referential sophistication you've described then you need one or more people in a Map Manager job role, responsible for setting key naming policy, key management policy and practice, and construction of the maps themselves, at least as regards key definitions.

Ideally, authors responsible for writing topics (and not for creating maps necessarily) should not have to worry about the key definition details.

Another thing I'll suggest is *put keyscope on everything*, at least on each chapter.

As I've used key scopes more I've come to appreciate how much they help--when you need them you can't do without them and when you don't need them they don't get in the way (or shouldn't).

But for example, by having a keyscope on each chapter, it allows you to easily refer to topics within a chapter from outside clearly and unambiguously. It can also serve to make the subject of a chapter clearer, e.g.:

<bookmap>
...
<chapter keyscope="installation" keys="installation" href="./installation/topic-001.dita">
<topicref keys="prereqs" href="./installation/topic-002.dita"/>
<topicref keys="remove-cover" href="./installation/topic-003.dita"/>
<topicref keys="insert-cart" href="./installation/topic-004.dita"/>
<topicref keys="replace-cover" href="./installation/topic-003.dita"/>
</chapter>
...
</bookmap>

From within the chapter, topics can refer to unqualified keys, i.e., <p>You must satisfy the prerequisites defined in <xref keyref="prereqs"/> ...</p>

And from other chapters you use the key scope: <p>Cover must be removed. See <xref keyref="installation.remove-cover"/>.</p>

Note that all the keys in this example are semantic while the topic filenames are arbitrary and meaningless. This separates the names used to identify and refer to topics in the map and the publication content from the topic storage details. The information nature of the topics will not change but their storage details *will change*.

When you said that my option #2 didn't work--what aspect of it didn't work? The cross-deliverable link is the same in both #1 and #2. The only thing option #2 provides is a way to have a scope-qualified reference work when the target key is in the same root map.

Finally, keep in mind that you can override any scope-qualified key definition by defining a higher-precedence key by defining a key where the key name includes the scope name, i.e.:

<>
<keydef keys="installation.prereqs" href="./topics/installation/prereq-for-ios15.dita" platform="iOS15"/>
...
<chapter keyscope="installation" ...>

Cheers,

E.
--
Eliot Kimber
http://contrext.com


On 6/9/21, 6:32 PM, "Chris Papademetrious" <main@dita-users.groups.io on behalf of chrispitude@gmail.com> wrote:

Thanks Eliot! Excellent summary of the problem and the situation.

Your method #2 (adding a map-level @keyscope) is what I tried, but it doesn't seem to work in Oxygen. It doesn't form the cross-book links properly, and if I create them by hand, they don't resolve.

I will try method #1 tomorrow. However, I'm not sure how well it would work in practice, as the writers want to share entire chapters between books, with a variety of cross-references to both book within the chapters. Trying to create all the local/cross-book flavors of indirection links would be quite maddening!

The good news is, your reply and suggestion for method #2 gives me the confidence that this should be fixable in Oxygen. Thank you so much!

- Chris


Chris Papademetrious
 

Hi Eliot,

The bug with Oxygen is when option #2 is used ((1) the current book A has its own local scope A, and (2) it references a peer book B with scope B, and (3) you drag-and-drop a topic from map B into a topic in map A to create a cross-book link), the link is incorrectly created as "B.B.topic1" instead of "B.topic1". SyncroSoft has confirmed it's a bug.

It would be hard to separate writers from their maps here. Each technical writer is assigned 1-3 products, and they own the user guides and reference manuals for those products. Because the map controls both PDF and OLH content delivery, the writers work with maps as tightly as they do the topics.

So, I try to empower the writers to manage their own maps by

  • Keeping map structures and rules as simple as possible
  • Using Schematron rules to keep writers on the right path
  • Relying on Oxygen to make things like cross-book link creation easy
So far, this combination has allowed us to be successful without the need for map/key managers and such, and without the writers having to get too much into the technical DITA stuff. For the most part, it's just writing and Oxygen GUI stuff. And I'll ride that for as long as I can! (And quite frankly, we're not resourced for anything more.)

We're already using peer-book scopes, so local scopes should be a straightforward conceptual extension for the writers to learn. Once this double-scoping bug is fixed in Oxygen, we should be in good shape for creating cross-book links in topics reused across books, which is really cool.

The next hurdle is creating cross-book links between books that use profiling conditions to publish one map to multiple deliverables. Writers want the ability to cross-link to specific conditional versions of books, and also to cross-link automatically between same-condition books. I've had some success prototyping this with a "wrapper map," which is a simple <map> that has only a <ditavalref> and a <mapref> to a full <bookmap>. I plan to share more about that here when the loose ends are ironed out.

The tip about books being able to provide overriding key definitions is a good one - thank you!

 - Chris


ekimber@contrext.com
 

Cross-deliverable links are a challenge for a number of reasons, but chief among them is the potential configuration complexity inherent in the fact that a single set of root maps can produce many different deliverables of different types and with different configurations. If there's an exact one-to-one mapping from source root maps to deliverables the problem is easy, but as soon as you can have two or more deliverables from a single root map, the problem gets much more challenging.

The DITA *source* markup makes it possible to unambiguously create a reference to any element in any topic in the context of a specific root map. This is necessary but not sufficient.

When you produce a given a deliverable from a root map you have to be able to control how the source-to-source links translate to deliverable-to-deliverable links when there are multiple possible deliverables for a given target root map.

That's exactly the scenario you described with the use of filtering applied to root maps to produce multiple deliverables from a single root map reflecting different filtering conditions.

Thus there has to be some way to configure the deliverable production process so that sets of related deliverables are produced correctly, i.e., for a set of inter-linked root maps, some way to ensure that the same filtering conditions (or the *correct* set of filtering conditions) is applied to a set of deliverable production processes so that the resulting deliverables are correctly linked to each other. This requires some sort of deliverable production project manager that gives you a way to configure the deliverable generation details.

For example, if you need to produce a set of interlinked deliverables that all reflect macOS, you need a way to specify not just the filtering but the deliverable URIs *as published* to that the links will be to the right place.

Using different wrapper maps that have different ditaval refs and result in different deliverable names is one way to do that and might be the easiest but it seems like that could quickly get either unwieldy or confusing or just impractical.

DITA OT's new project facility seems like at least a start for this kind of production processing configuration manager but more is probably required to fully coordinate the production of multiple interlinked deliverables that have different input parameters (different filtering conditions, etc.) and different result details (different publishing locations for the deliverables).

It sounds like you're able to impose simplifying assumptions that make the problem easier to solve in your environment, which is good--keeping it as simple as you can and still meeting requirements is always a good idea.

A challenge for a tool like DITA OT is that a more general solution starts to become pretty complex pretty quickly, which makes it less likely anyone will take a stab at implementing it...

Cheers,

E.

--
Eliot Kimber
http://contrext.com


On 6/13/21, 8:29 PM, "Chris Papademetrious" <main@dita-users.groups.io on behalf of chrispitude@gmail.com> wrote:

Hi Eliot,

The bug with Oxygen is when option #2 is used ((1) the current book A has its own local scope A, and (2) it references a peer book B with scope B, and (3) you drag-and-drop a topic from map B into a topic in map A to create a cross-book link), the link is incorrectly created as "B.B.topic1" instead of "B.topic1". SyncroSoft has confirmed it's a bug.

It would be hard to separate writers from their maps here. Each technical writer is assigned 1-3 products, and they own the user guides and reference manuals for those products. Because the map controls both PDF and OLH content delivery, the writers work with maps as tightly as they do the topics.

So, I try to empower the writers to manage their own maps by


* Keeping map structures and rules as simple as possible
* Using Schematron rules to keep writers on the right path
* Relying on Oxygen to make things like cross-book link creation easy

So far, this combination has allowed us to be successful without the need for map/key managers and such, and without the writers having to get too much into the technical DITA stuff. For the most part, it's just writing and Oxygen GUI stuff. And I'll ride that for as long as I can! (And quite frankly, we're not resourced for anything more.)

We're already using peer-book scopes, so local scopes should be a straightforward conceptual extension for the writers to learn. Once this double-scoping bug is fixed in Oxygen, we should be in good shape for creating cross-book links in topics reused across books, which is really cool.

The next hurdle is creating cross-book links between books that use profiling conditions to publish one map to multiple deliverables. Writers want the ability to cross-link to specific conditional versions of books, and also to cross-link automatically between same-condition books. I've had some success prototyping this with a "wrapper map," which is a simple <map> that has only a <ditavalref> and a <mapref> to a full <bookmap>. I plan to share more about that here when the loose ends are ironed out.

The tip about books being able to provide overriding key definitions is a good one - thank you!

- Chris


Chris Papademetrious
 

Hi everyone,

To follow up on this discussion, we found the following peer map bugs in Oxygen 23:

  • When @keyscope is defined on a peer map reference and the target map defines its own local @keyscope on its root map element, Oxygen 23 incorrectly concatenates them into two levels of scope during link creation and validation. (Per 2.3.4.5 "Cross-deliverable addressing and linking", the target map's scope should be ignored in this case.)
  • When @conkeyref references into peer maps exist, Oxygen 23 treats the references as resolved (as if the map would be included and processed), when instead it should only be resolved if that map is locally included elsewhere in the publication.

These bugs will be fixed in Oxygen 24. (Syncro Soft was kind enough to allow us to beta-test the fixes.)

With these fixes, I can (1) define a local keyscope on a map, then (2) create scoped links in shared topics that are local or cross-book, depending on which book includes the topic. This satisfies the need in the original post - hooray!

 - Chris