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

MeindertN / RoboClerk / 20514466406

26 Dec 2025 02:27AM UTC coverage: 68.657% (-0.8%) from 69.504%
20514466406

push

github

MeindertN
WIP: fixed broken tests and fixed some bugs in the text tag parsing code.

2320 of 3371 branches covered (68.82%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 1 file covered. (100.0%)

723 existing lines in 26 files now uncovered.

7443 of 10849 relevant lines covered (68.61%)

84.69 hits per line

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

93.6
/RoboClerk.Core/ItemTemplateSupport/ScriptingBridge.cs
1
using RoboClerk.Core;
2
using RoboClerk.Core.Configuration;
3
using System;
4
using System.Collections.Generic;
5
using System.Linq;
6
using System.Text;
7
using System.Text.RegularExpressions;
8

9
namespace RoboClerk
10
{
11
    public class ScriptingBridge<T> where T : Item
12
    {
13
        private IDataSources data = null!;
102✔
14
        private ITraceabilityAnalysis analysis = null!;
102✔
15
        private IConfiguration configuration = null!;
102✔
16
        private List<string> traces = new List<string>();
102✔
17
        private List<T> items = new List<T>();
102✔
18
        private IRoboClerkTag currentTag = null;
102✔
19
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
1✔
20

21
        public ScriptingBridge(IDataSources data, ITraceabilityAnalysis trace, TraceEntity sourceTraceEntity, IConfiguration config)
102✔
22
        {
102✔
23
            this.data = data;
102✔
24
            analysis = trace;
102✔
25
            configuration = config;
102✔
26
            SourceTraceEntity = sourceTraceEntity;
102✔
27
        }
102✔
28

29
        /// <summary>
30
        /// The item that needs to be rendered in the documentation. 
31
        /// </summary>
32
        public T Item { get; set; } = default!;
145✔
33

34
        /// <summary>
35
        /// The items that need to be rendered in the documentation (empty if there is only a single item). 
36
        /// </summary>
37
        public IEnumerable<T> Items
38
        {
39
            get { return items; }
81✔
40
            set { items = value.ToList(); }
81✔
41
        }
42

43
        /// <summary>
44
        /// Indicates if the current item is the first item in the list of items being rendered.
45
        /// </summary>
46
        public bool IsFirst { get; set; }
22✔
47

48
        /// <summary>
49
        /// Indicates if the current item is the last item in the list of items being rendered.
50
        /// </summary>
51
        public bool IsLast { get; set; }
22✔
52

53
        /// <summary>
54
        /// The index of the current item in the list of items being rendered (0-based).
55
        /// </summary>
56
        public int Index { get; set; }
22✔
57

58
        /// <summary>
59
        /// The total number of items being rendered.
60
        /// </summary>
61
        public int Count { get; set; }
22✔
62

63
        /// <summary>
64
        /// The RoboClerk tag that triggered the template rendering. Provides access to tag parameters.
65
        /// </summary>
66
        public IRoboClerkTag Tag
67
        {
68
            get { return currentTag; }
9✔
69
            set { currentTag = value; }
201✔
70
        }
71

72
        /// <summary>
73
        /// This function adds a trace link to the item who's ID is provided in the id parameter.
74
        /// </summary>
75
        /// <param name="id"></param>
76
        public void AddTrace(string id)
77
        {
76✔
78
            traces.Add(id);
76✔
79
        }
76✔
80

81
        /// <summary>
82
        /// Returns all the traces in the bridge.
83
        /// </summary>
84
        public IEnumerable<string> Traces
85
        {
86
            get { return traces; }
108✔
87
        }
88

89
        /// <summary>
90
        /// Returns the source trace entity, indicates what kind of trace item is being rendered.
91
        /// </summary>
92
        public TraceEntity SourceTraceEntity { get; }
48✔
93

94
        /// <summary>
95
        /// Provides access to the RoboClerk datasources object. This can be used to search in all 
96
        /// the information RoboClerk has.
97
        /// </summary>
UNCOV
98
        public IDataSources Sources { get { return data; } }
×
99

100
        /// <summary>
101
        /// Provides access to the traceability analysis object. The object stores all traceability
102
        /// information in RoboClerk.
103
        /// </summary>
104
        public ITraceabilityAnalysis TraceabilityAnalysis { get { return analysis; } }
69✔
105

106
        /// <summary>
107
        /// Gets a parameter value from the current tag, returning the default value if the parameter doesn't exist.
108
        /// </summary>
109
        /// <param name="parameterName">The name of the parameter to retrieve</param>
110
        /// <param name="defaultValue">The default value to return if parameter doesn't exist</param>
111
        /// <returns>The parameter value or default value</returns>
112
        public string GetTagParameter(string parameterName, string defaultValue = "")
113
        {
19✔
114
            if (currentTag == null)
19✔
115
            {
2✔
116
                logger.Error($"Attempted to get tag parameter '{parameterName}' but no tag is set in ScriptingBridge");
2✔
117
                throw new InvalidOperationException("No tag is set in ScriptingBridge.");
2✔
118
            }
119
            return currentTag.GetParameterOrDefault(parameterName, defaultValue);
17✔
120
        }
17✔
121

122
        /// <summary>
123
        /// Checks if the current tag has a specific parameter.
124
        /// </summary>
125
        /// <param name="parameterName">The name of the parameter to check for</param>
126
        /// <returns>True if the parameter exists, false otherwise</returns>
127
        public bool HasTagParameter(string parameterName)
128
        {
6✔
129
            if (currentTag == null)
6✔
130
            {
1✔
131
                logger.Error($"Attempted to check for tag parameter '{parameterName}' but no tag is set in ScriptingBridge");
1✔
132
                throw new InvalidOperationException("No tag is set in ScriptingBridge.");
1✔
133
            }
134
            return currentTag.HasParameter(parameterName);
5✔
135
        }
5✔
136

137
        /// <summary>
138
        /// Gets all parameter names from the current tag.
139
        /// </summary>
140
        /// <returns>Collection of all parameter names, or empty collection if no tag is set</returns>
141
        public IEnumerable<string> GetAllTagParameterNames()
142
        {
2✔
143
            if (currentTag == null)
2✔
144
            {
1✔
145
                logger.Error("Attempted to get all tag parameter names but no tag is set in ScriptingBridge");
1✔
146
                throw new InvalidOperationException("No tag is set in ScriptingBridge.");
1✔
147
            }
148
            return currentTag.Parameters;
1✔
149
        }
1✔
150

151
        /// <summary>
152
        /// Returns all linked items attached to li with a link type of linkType
153
        /// </summary>
154
        /// <param name="li"></param>
155
        /// <param name="linkType"></param>
156
        /// <returns></returns>
157
        public IEnumerable<LinkedItem> GetLinkedItems(LinkedItem li, ItemLinkType linkType)
158
        {
43✔
159
            List<LinkedItem> results = new List<LinkedItem>();
43✔
160
            var linkedItems = li.LinkedItems.Where(x => x.LinkType == linkType);
51✔
161
            foreach (var item in linkedItems)
135✔
162
            {
3✔
163
                var linkedItem = (LinkedItem)data.GetItem(item.TargetID);
3✔
164
                if (linkedItem != null)
3✔
165
                {
3✔
166
                    results.Add(linkedItem);
3✔
167
                }
3✔
168
            }
3✔
169
            return results;
43✔
170
        }
43✔
171

172
        /// <summary>
173
        /// This function retrieves all items linked to li with a link of type linkType and returns an
174
        /// asciidoc string that has links to all these items. Usually used to provide trace information.
175
        /// You can use includeTitle to control if the title of the linked item is included as well.
176
        /// </summary>
177
        /// <param name="li"></param>
178
        /// <param name="linkType"></param>
179
        /// <param name="includeTitle"</param>
180
        /// <returns>AsciiDoc string with http link to item.</returns>
181
        /// <exception cref="System.Exception"></exception>
182
        public string GetLinkedField(LinkedItem li, ItemLinkType linkType, bool includeTitle = true)
183
        {
20✔
184
            StringBuilder field = new StringBuilder();
20✔
185
            var linkedItems = li.LinkedItems.Where(x => x.LinkType == linkType);
44✔
186
            if (linkedItems.Count() > 0)
20✔
187
            {
11✔
188
                foreach (var item in linkedItems)
55✔
189
                {
12✔
190
                    if (field.Length > 0)
12✔
191
                    {
1✔
192
                        field.Append(" / ");
1✔
193
                    }
1✔
194
                    var linkedItem = data.GetItem(item.TargetID);
12✔
195
                    if (linkedItem != null)
12✔
196
                    {
10✔
197
                        AddTrace(linkedItem.ItemID);
10✔
198
                        field.Append(linkedItem.HasLink ? GetItemLinkString(linkedItem) : linkedItem.ItemID);
10✔
199
                        if (includeTitle && linkedItem.ItemTitle != string.Empty)
10✔
200
                        {
8✔
201
                            field.Append($": \"{linkedItem.ItemTitle}\"");
8✔
202
                        }
8✔
203
                    }
10✔
204
                    else
205
                    {
2✔
206
                        logger.Error($"Item with ID {li.ItemID} has a trace link to item with ID {item.TargetID} but that item does not exist.");
2✔
207
                        throw new System.Exception("Item with invalid trace link encountered.");
2✔
208
                    }
209
                }
10✔
210
                return field.ToString();
9✔
211
            }
212
            return "N/A";
9✔
213
        }
18✔
214

215
        /// <summary>
216
        /// Returns a hyperlink string for the provided item, formatted according to the configured output format.
217
        /// </summary>
218
        /// <param name="item">The item for which to generate a hyperlink.</param>
219
        /// <returns>
220
        /// A formatted hyperlink string. If the output format is HTML, returns an &lt;a&gt; tag.
221
        /// If the format is ASCIIDOC, returns an AsciiDoc-style link. 
222
        /// If the item has no link, returns just the item ID.
223
        /// </returns>
224
        /// <exception cref="NotSupportedException">Thrown if the configured output format is not supported.</exception>
225
        public string GetItemLinkString(Item item)
226
        {
97✔
227
            string format = configuration.OutputFormat.ToUpper();
97✔
228
            string result = item.ItemID;
97✔
229
            if (item.HasLink)
97✔
230
            {
21✔
231
                if (format == "HTML" || format == "DOCX")
21!
UNCOV
232
                {
×
UNCOV
233
                    result = $"<a href=\"{item.Link}\">{item.ItemID}</a>";
×
UNCOV
234
                }
×
235
                else if (format == "ASCIIDOC")
21!
236
                {
21✔
237
                    result = $"{item.Link}[{item.ItemID}]";
21✔
238
                }
21✔
239
                else
UNCOV
240
                {
×
UNCOV
241
                    logger.Warn($"Unknown output format \"{format}\" specified. Links may be missing from output.");
×
UNCOV
242
                }
×
243
            }
21✔
244
            return result;
97✔
245
        }
97✔
246

247
        /// <summary>
248
        /// Convenience function, checks if value is an empty string, if so, the
249
        /// defaultValue is returned.
250
        /// </summary>
251
        /// <param name="value"></param>
252
        /// <param name="defaultValue"></param>
253
        /// <returns></returns>
254
        public string GetValOrDef(string value, string defaultValue)
255
        {
33✔
256
            if (value == string.Empty)
33✔
257
            {
16✔
258
                return defaultValue;
16✔
259
            }
260
            return value;
17✔
261
        }
33✔
262

263
        /// <summary>
264
        /// Convenience function, calls ToString on input and returns resulting string.
265
        /// </summary>
266
        /// <param name="input"></param>
267
        /// <returns>string representation</returns>
268
        public string Insert(object? input)
269
        {
2✔
270
            return input?.ToString() ?? string.Empty;
2!
271
        }
2✔
272

273
        /// <summary>
274
        /// Convenience function, combines other convenience functions and ensures that
275
        /// ASCIDOC meant to be inside a table cell will render in an appropriate way by
276
        /// embedding tables and removing embedded headings because ASCIIDOC does not 
277
        /// support scoped headings and most likely the user does not want to have
278
        /// embedded headings numbered with the document headers.
279
        /// </summary>
280
        /// <param name="input">The AsciiDoc content that may contain headings and 
281
        /// or tables</param>
282
        /// <returns>Modified AsciiDoc that can be embedded in a table cell</returns>
283
        public string ProcessAsciidocForTableCell(string input)
284
        {
6✔
285
            string temp = ConvertHeadingsForASCIIDOCTableCell(input);
6✔
286
            return EmbedAsciidocTables(temp);
6✔
287
        }
6✔
288

289
        /// <summary>
290
        /// Convenience function, takes any asciidoc tables in the input and makes them
291
        /// embedded tables. 
292
        /// </summary>
293
        /// <param name="input"></param>
294
        /// <returns></returns>
295
        public string EmbedAsciidocTables(string input)
296
        {
12✔
297
            // Split the input into lines (preserving newlines)
298
            var lines = input.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
12✔
299
            var outputLines = new List<string>();
12✔
300
            bool inTable = false;
12✔
301

302
            foreach (var line in lines)
102✔
303
            {
33✔
304
                string trimmed = line.Trim();
33✔
305

306
                // Detect the table delimiter
307
                if (trimmed == "|===")
33✔
308
                {
12✔
309
                    inTable = !inTable;
12✔
310
                    // Replace outer table delimiter with nested table delimiter
311
                    outputLines.Add(line.Replace("|===", "!==="));
12✔
312
                }
12✔
313
                else if (inTable)
21✔
314
                {
7✔
315
                    // Preserve leading whitespace
316
                    int leadingSpaces = line.Length - line.TrimStart().Length;
7✔
317
                    string converted = line;
7✔
318

319
                    // If the cell begins with a pipe, replace it with an exclamation mark,
320
                    // but only if the pipe is not escaped.
321
                    if (line.TrimStart().StartsWith("|"))
7✔
322
                    {
7✔
323
                        converted = new string(' ', leadingSpaces) + "!" + line.TrimStart().Substring(1);
7✔
324
                    }
7✔
325

326
                    // Replace any unescaped cell separator pipe.
327
                    // The regex (?<!\\)\| matches any pipe that is not preceded by a backslash.
328
                    converted = Regex.Replace(converted, @"(?<!\\)\|", "!");
7✔
329
                    outputLines.Add(converted);
7✔
330
                }
7✔
331
                else
332
                {
14✔
333
                    // Outside a table block, leave the line unchanged.
334
                    outputLines.Add(line);
14✔
335
                }
14✔
336
            }
33✔
337

338
            // Rejoin all lines into a single string.
339
            return string.Join("\n", outputLines);
12✔
340
        }
12✔
341

342
        /// <summary>
343
        /// Converts AsciiDoc heading syntax to alternative markup suitable for embedding in table cells.
344
        /// This prevents heading content from being numbered along with main document headings.
345
        /// </summary>
346
        /// <param name="input">The AsciiDoc content that may contain headings</param>
347
        /// <returns>Modified AsciiDoc with headings converted to alternative markup</returns>
348
        public string ConvertHeadingsForASCIIDOCTableCell(string input)
349
        {
17✔
350
            if (string.IsNullOrEmpty(input))
17✔
351
                return input;
2✔
352

353
            // Process each line to detect and transform headings
354
            string[] lines = input.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
15✔
355
            for (int i = 0; i < lines.Length; i++)
96✔
356
            {
33✔
357
                string line = lines[i];
33✔
358

359
                // Match heading patterns (e.g., "== Heading", "=== Subheading", etc.)
360
                var match = System.Text.RegularExpressions.Regex.Match(line, @"^(=+)\s+(.+)$");
33✔
361
                if (match.Success)
33✔
362
                {
10✔
363
                    int level = match.Groups[1].Length;
10✔
364
                    string headingText = match.Groups[2].Value.Trim();
10✔
365

366
                    // Convert heading based on its level
367
                    switch (level)
10!
368
                    {
369
                        case 1: // Document title level
370
                        case 2: // Top section level - convert to bold
371
                            lines[i] = $"*{headingText}*";
4✔
372
                            break;
4✔
373
                        case 3: // Subsection level - convert to italic
374
                            lines[i] = $"_{headingText}_";
2✔
375
                            break;
2✔
376
                        case 4: // Subsubsection level - convert to italic with indentation
377
                            lines[i] = $"&#160;&#160; _{headingText}_";
2✔
378
                            break;
2✔
379
                        case 5: // Even deeper levels - use monospace with indentation
380
                        case 6:
381
                            lines[i] = $"&#160;&#160;&#160;&#160; `{headingText}`";
2✔
382
                            break;
2✔
383
                        default: // For extremely deep levels or unexpected cases
UNCOV
384
                            lines[i] = headingText;
×
UNCOV
385
                            break;
×
386
                    }
387

388
                    // Add a blank line after the heading for better readability
389
                    // only if there isn't already one and we're not at the end of the text
390
                    if (i < lines.Length - 1 && !string.IsNullOrWhiteSpace(lines[i + 1]))
10!
391
                    {
9✔
392
                        lines[i] += "\n";
9✔
393
                    }
9✔
394
                }
10✔
395
            }
33✔
396

397
            return string.Join("\n", lines);
15✔
398
        }
17✔
399
    }
400

401
    
402

403
    public class ScriptingBridge : ScriptingBridge<LinkedItem>
404
    {
405
        public ScriptingBridge(IDataSources data, ITraceabilityAnalysis trace, TraceEntity sourceTraceEntity, IConfiguration config)
406
            : base(data, trace, sourceTraceEntity, config)
101✔
407
        {
101✔
408
        }
101✔
409
    }
410
}
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