• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

knowledgepixels / nanodash / 24438916206

15 Apr 2026 05:57AM UTC coverage: 15.843% (-0.01%) from 15.856%
24438916206

push

github

tkuhn
feat: also strip near-matching parent prefixes from breadcrumb labels

Replace the strict "child starts with parent + space" check with a
longest-common-prefix match that tolerates small endings on the parent
(e.g. "Nano Sessions" / "Nano Session #30" now renders as
"Nano Sessions" > "#30"). Guarded by a min-3-char match, a
within-2-chars-of-parent-length requirement, and a non-letter/digit
boundary in the child to avoid mid-word stripping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

790 of 6158 branches covered (12.83%)

Branch coverage included in aggregate %.

1979 of 11320 relevant lines covered (17.48%)

2.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

25.61
src/main/java/com/knowledgepixels/nanodash/component/TitleBar.java
1
package com.knowledgepixels.nanodash.component;
2

3
import java.util.ArrayList;
4
import java.util.List;
5

6
import org.apache.wicket.behavior.AttributeAppender;
7
import org.apache.wicket.markup.html.WebMarkupContainer;
8
import org.apache.wicket.markup.html.panel.Panel;
9
import org.apache.wicket.markup.repeater.Item;
10
import org.apache.wicket.markup.repeater.data.DataView;
11
import org.apache.wicket.markup.repeater.data.ListDataProvider;
12

13
import com.knowledgepixels.nanodash.NanodashPageRef;
14
import com.knowledgepixels.nanodash.NanodashPreferences;
15
import com.knowledgepixels.nanodash.Utils;
16
import com.knowledgepixels.nanodash.page.NanodashPage;
17

18
/**
19
 * TitleBar is the top bar of the Nanodash application, which contains
20
 * navigation elements such as profile, my channel, users, connectors,
21
 * publish, query, and breadcrumb navigation.
22
 */
23
public class TitleBar extends Panel {
24

25
    private String highlight;
26

27
    /**
28
     * Constructs a TitleBar with the specified id, page, highlight element,
29
     * and an array of path references for breadcrumb navigation.
30
     *
31
     * @param id        the component id
32
     * @param page      the current Nanodash page
33
     * @param highlight the id of the element to highlight
34
     * @param pathRefs  an array of NanodashPageRef for breadcrumb navigation
35
     */
36
    public TitleBar(String id, NanodashPage page, String highlight, NanodashPageRef... pathRefs) {
37
        super(id);
9✔
38
        this.highlight = highlight;
9✔
39
        add(new ProfileItem("profile", page));
39✔
40

41
        createContainer("users");
12✔
42
        createContainer("connectors");
12✔
43
        createContainer("publish").setVisible(!NanodashPreferences.get().isReadOnlyMode());
30!
44
        createContainer("query");
12✔
45

46
        WebMarkupContainer breadcrumbPath = new WebMarkupContainer("breadcrumbpath");
15✔
47
        breadcrumbPath.setVisible(pathRefs.length > 0);
21!
48
        if (pathRefs.length > 0) {
9!
49
            final String[] displayLabels = simplifyBreadcrumbLabels(pathRefs);
×
50
            breadcrumbPath.add(pathRefs[0].createComponent("firstpathelement", displayLabels[0]));
×
51
            // Getting serialization exception if not using 'new ArrayList<...>(...)' here:
52
            List<NanodashPageRef> morePathElements = new ArrayList<NanodashPageRef>(Utils.subList(pathRefs, 1, pathRefs.length));
×
53
            breadcrumbPath.add(new DataView<NanodashPageRef>("morepathelements", new ListDataProvider<NanodashPageRef>(morePathElements)) {
×
54

55
                @Override
56
                protected void populateItem(Item<NanodashPageRef> item) {
57
                    int index = (int) item.getIndex() + 1;
×
58
                    item.add(item.getModelObject().createComponent("furtherpathelement", displayLabels[index]));
×
59
                }
×
60

61
            });
62
        } else {
×
63
            breadcrumbPath.setVisible(false);
12✔
64
        }
65
        add(breadcrumbPath);
27✔
66
    }
3✔
67

68
    /**
69
     * Computes shortened display labels for a breadcrumb path.
70
     *
71
     * Two simplifications are applied:
72
     * <ul>
73
     *   <li>For each non-root crumb, the part it shares with its parent label
74
     *       is stripped (e.g. parent "Knowledge Pixels", child "Knowledge
75
     *       Pixels Incubator" renders as "Incubator"). The shared part is the
76
     *       longest common character prefix, but only stripped when it covers
77
     *       (almost) all of the parent and ends on a non-letter/digit boundary
78
     *       in the child — this also catches singular/plural variations like
79
     *       parent "Nano Sessions", child "Nano Session #30" → "#30".</li>
80
     *   <li>Any ": " in a label and everything after it is removed (e.g.
81
     *       "Incubator 1: Some title" becomes "Incubator 1"). Applied to all
82
     *       crumbs, including the first.</li>
83
     * </ul>
84
     *
85
     * The parent-prefix comparison uses the original (un-simplified) labels,
86
     * so that simplifications on the parent don't interfere with the child.
87
     */
88
    static String[] simplifyBreadcrumbLabels(NanodashPageRef[] pathRefs) {
89
        String[] displayLabels = new String[pathRefs.length];
×
90
        for (int i = 0; i < pathRefs.length; i++) {
×
91
            String label = pathRefs[i].getLabel();
×
92
            if (label != null) {
×
93
                if (i > 0) {
×
94
                    label = stripParentPrefix(pathRefs[i - 1].getLabel(), label);
×
95
                }
96
                int colonIdx = label.indexOf(": ");
×
97
                if (colonIdx > 0) {
×
98
                    label = label.substring(0, colonIdx);
×
99
                }
100
            }
101
            displayLabels[i] = label;
×
102
        }
103
        return displayLabels;
×
104
    }
105

106
    /**
107
     * If the child label appears to restate the parent's "topic" at the start,
108
     * strips that shared prefix and returns the remainder. Otherwise returns
109
     * the child label unchanged.
110
     */
111
    private static String stripParentPrefix(String parent, String child) {
112
        if (parent == null || parent.isEmpty() || child == null) return child;
×
113
        int lcp = 0;
×
114
        int max = Math.min(parent.length(), child.length());
×
115
        while (lcp < max && parent.charAt(lcp) == child.charAt(lcp)) lcp++;
×
116
        // Require a substantial match: at least 3 chars, and within 2 chars of
117
        // the full parent length (so things like "Sessions" vs "Session" still
118
        // match, but "Nanopublication Sessions" vs "Nano Session #30" doesn't).
119
        if (lcp < 3 || lcp < parent.length() - 2) return child;
×
120
        // The boundary in the child must not fall mid-word, otherwise we'd be
121
        // chopping off a partial word like "Foo Bar" -> "Foo Baz Quux" -> "z Quux".
122
        if (lcp >= child.length() || Character.isLetterOrDigit(child.charAt(lcp))) return child;
×
123
        String remainder = child.substring(lcp).replaceAll("^\\s+", "");
×
124
        if (remainder.isEmpty()) return child;
×
125
        return remainder;
×
126
    }
127

128
    private WebMarkupContainer createContainer(String id) {
129
        WebMarkupContainer c = new WebMarkupContainer(id);
15✔
130
        if (id.equals(highlight)) {
15!
131
            c.add(new AttributeAppender("class", "selected"));
×
132
        }
133
        add(c);
27✔
134
        return c;
6✔
135
    }
136

137
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc