lula2 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +291 -8
- package/dist/_app/env.js +1 -0
- package/dist/_app/immutable/assets/0.DtiRW3lO.css +1 -0
- package/dist/_app/immutable/assets/DynamicControlEditor.BkVTzFZ-.css +1 -0
- package/dist/_app/immutable/chunks/7x_q-1ab.js +1 -0
- package/dist/_app/immutable/chunks/B19gt6-g.js +2 -0
- package/dist/_app/immutable/chunks/BR-0Dorr.js +1 -0
- package/dist/_app/immutable/chunks/B_3ksxz5.js +2 -0
- package/dist/_app/immutable/chunks/Bg_R1qWi.js +3 -0
- package/dist/_app/immutable/chunks/D3aNP_lg.js +1 -0
- package/dist/_app/immutable/chunks/D4Q_ObIy.js +1 -0
- package/dist/_app/immutable/chunks/DsnmJJEf.js +1 -0
- package/dist/_app/immutable/chunks/XY2j_owG.js +66 -0
- package/dist/_app/immutable/chunks/rzN25oDf.js +1 -0
- package/dist/_app/immutable/entry/app.r0uOd9qg.js +2 -0
- package/dist/_app/immutable/entry/start.DvoqR0rc.js +1 -0
- package/dist/_app/immutable/nodes/0.Ct6FAss_.js +1 -0
- package/dist/_app/immutable/nodes/1.DLoKuy8Q.js +1 -0
- package/dist/_app/immutable/nodes/2.IRkwSmiB.js +1 -0
- package/dist/_app/immutable/nodes/3.BrTg-ZHv.js +1 -0
- package/dist/_app/immutable/nodes/4.Blq-4WQS.js +9 -0
- package/dist/_app/version.json +1 -0
- package/dist/cli/commands/crawl.js +128 -0
- package/dist/cli/commands/ui.js +2769 -0
- package/dist/cli/commands/version.js +30 -0
- package/dist/cli/server/index.js +2713 -0
- package/dist/cli/server/server.js +2702 -0
- package/dist/cli/server/serverState.js +1199 -0
- package/dist/cli/server/spreadsheetRoutes.js +788 -0
- package/dist/cli/server/types.js +0 -0
- package/dist/cli/server/websocketServer.js +2625 -0
- package/dist/cli/utils/debug.js +24 -0
- package/dist/favicon.svg +1 -0
- package/dist/index.html +38 -0
- package/dist/index.js +2924 -37
- package/dist/lula.png +0 -0
- package/dist/lula2 +2 -0
- package/package.json +120 -72
- package/src/app.css +192 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +13 -0
- package/src/lib/actions/fadeWhenScrollable.ts +39 -0
- package/src/lib/actions/modal.ts +230 -0
- package/src/lib/actions/tooltip.ts +82 -0
- package/src/lib/components/control-sets/ControlSetInfo.svelte +20 -0
- package/src/lib/components/control-sets/ControlSetSelector.svelte +46 -0
- package/src/lib/components/control-sets/index.ts +5 -0
- package/src/lib/components/controls/ControlDetailsPanel.svelte +235 -0
- package/src/lib/components/controls/ControlsList.svelte +608 -0
- package/src/lib/components/controls/DynamicControlEditor.svelte +298 -0
- package/src/lib/components/controls/MappingCard.svelte +105 -0
- package/src/lib/components/controls/MappingForm.svelte +188 -0
- package/src/lib/components/controls/index.ts +9 -0
- package/src/lib/components/controls/renderers/EditableFieldRenderer.svelte +103 -0
- package/src/lib/components/controls/renderers/FieldRenderer.svelte +49 -0
- package/src/lib/components/controls/renderers/index.ts +5 -0
- package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +130 -0
- package/src/lib/components/controls/tabs/ImplementationTab.svelte +127 -0
- package/src/lib/components/controls/tabs/MappingsTab.svelte +182 -0
- package/src/lib/components/controls/tabs/OverviewTab.svelte +151 -0
- package/src/lib/components/controls/tabs/TimelineTab.svelte +41 -0
- package/src/lib/components/controls/tabs/index.ts +8 -0
- package/src/lib/components/controls/utils/ProcessedTextRenderer.svelte +63 -0
- package/src/lib/components/controls/utils/textProcessor.ts +164 -0
- package/src/lib/components/forms/DynamicControlForm.svelte +340 -0
- package/src/lib/components/forms/DynamicField.svelte +494 -0
- package/src/lib/components/forms/FormField.svelte +107 -0
- package/src/lib/components/forms/index.ts +6 -0
- package/src/lib/components/setup/ExistingControlSets.svelte +284 -0
- package/src/lib/components/setup/SpreadsheetImport.svelte +968 -0
- package/src/lib/components/setup/index.ts +5 -0
- package/src/lib/components/ui/Dropdown.svelte +107 -0
- package/src/lib/components/ui/EmptyState.svelte +80 -0
- package/src/lib/components/ui/FeatureToggle.svelte +50 -0
- package/src/lib/components/ui/SearchBar.svelte +73 -0
- package/src/lib/components/ui/StatusBadge.svelte +79 -0
- package/src/lib/components/ui/TabNavigation.svelte +48 -0
- package/src/lib/components/ui/Tooltip.svelte +120 -0
- package/src/lib/components/ui/index.ts +10 -0
- package/src/lib/components/version-control/DiffViewer.svelte +292 -0
- package/src/lib/components/version-control/TimelineItem.svelte +107 -0
- package/src/lib/components/version-control/YamlDiffViewer.svelte +428 -0
- package/src/lib/components/version-control/index.ts +6 -0
- package/src/lib/form-types.ts +57 -0
- package/src/lib/formatUtils.ts +17 -0
- package/src/lib/index.ts +5 -0
- package/src/lib/types.ts +180 -0
- package/src/lib/websocket.ts +359 -0
- package/src/routes/+layout.svelte +236 -0
- package/src/routes/+page.svelte +38 -0
- package/src/routes/control/[id]/+page.svelte +112 -0
- package/src/routes/setup/+page.svelte +241 -0
- package/src/stores/compliance.ts +95 -0
- package/src/styles/highlightjs.css +20 -0
- package/src/styles/modal.css +58 -0
- package/src/styles/tables.css +111 -0
- package/src/styles/tooltip.css +65 -0
- package/dist/controls/index.d.ts +0 -18
- package/dist/controls/index.d.ts.map +0 -1
- package/dist/controls/index.js +0 -18
- package/dist/crawl.d.ts +0 -62
- package/dist/crawl.d.ts.map +0 -1
- package/dist/crawl.js +0 -172
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/src/controls/index.ts +0 -19
- package/src/crawl.ts +0 -227
- package/src/index.ts +0 -46
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import type { Control, FieldSchema } from '$lib/types';
|
|
6
|
+
import { FieldRenderer } from '../renderers';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
control: Control;
|
|
10
|
+
fieldSchema: Record<string, FieldSchema>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { control, fieldSchema }: Props = $props();
|
|
14
|
+
|
|
15
|
+
// Get fields for overview tab
|
|
16
|
+
function getOverviewFields(): Array<[string, FieldSchema]> {
|
|
17
|
+
return Object.entries(fieldSchema)
|
|
18
|
+
.filter(([fieldName, field]) => {
|
|
19
|
+
// Exclude id and family fields from overview since they're in the header
|
|
20
|
+
if (fieldName === 'id' || fieldName === 'family') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
// Map category to tab if tab not explicitly set
|
|
24
|
+
const fieldTab = field.tab || getDefaultTabForCategory(field.category);
|
|
25
|
+
return fieldTab === 'overview' && field.visible;
|
|
26
|
+
})
|
|
27
|
+
.sort((a, b) => a[1].display_order - b[1].display_order);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getDefaultTabForCategory(category: string): 'overview' | 'implementation' | 'custom' {
|
|
31
|
+
switch (category) {
|
|
32
|
+
case 'core':
|
|
33
|
+
case 'metadata':
|
|
34
|
+
return 'overview';
|
|
35
|
+
case 'compliance':
|
|
36
|
+
case 'content':
|
|
37
|
+
return 'implementation';
|
|
38
|
+
default:
|
|
39
|
+
return 'custom';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Helper to determine field layout class based on field type
|
|
44
|
+
function getFieldLayoutClass(field: FieldSchema): string {
|
|
45
|
+
// Textareas and long text fields get full width
|
|
46
|
+
if (field.ui_type === 'textarea' || field.ui_type === 'long_text') {
|
|
47
|
+
return 'col-span-full';
|
|
48
|
+
}
|
|
49
|
+
// Medium text fields also get full width
|
|
50
|
+
if (field.ui_type === 'medium_text' && field.max_length && field.max_length > 100) {
|
|
51
|
+
return 'col-span-full';
|
|
52
|
+
}
|
|
53
|
+
// Dropdowns and short fields can be side by side
|
|
54
|
+
if (
|
|
55
|
+
field.ui_type === 'select' ||
|
|
56
|
+
field.ui_type === 'boolean' ||
|
|
57
|
+
field.ui_type === 'date' ||
|
|
58
|
+
field.ui_type === 'number' ||
|
|
59
|
+
(field.ui_type === 'short_text' && field.max_length && field.max_length <= 50)
|
|
60
|
+
) {
|
|
61
|
+
return 'col-span-1';
|
|
62
|
+
}
|
|
63
|
+
// Default to full width for everything else
|
|
64
|
+
return 'col-span-full';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Helper to group fields by layout type
|
|
68
|
+
function groupFieldsForLayout(fields: Array<[string, FieldSchema]>) {
|
|
69
|
+
const groups: Array<Array<[string, FieldSchema]>> = [];
|
|
70
|
+
let currentGroup: Array<[string, FieldSchema]> = [];
|
|
71
|
+
|
|
72
|
+
for (const field of fields) {
|
|
73
|
+
const layoutClass = getFieldLayoutClass(field[1]);
|
|
74
|
+
|
|
75
|
+
if (layoutClass === 'col-span-full') {
|
|
76
|
+
// Full width fields go in their own group
|
|
77
|
+
if (currentGroup.length > 0) {
|
|
78
|
+
groups.push(currentGroup);
|
|
79
|
+
currentGroup = [];
|
|
80
|
+
}
|
|
81
|
+
groups.push([field]);
|
|
82
|
+
} else {
|
|
83
|
+
// Half width fields can be grouped
|
|
84
|
+
currentGroup.push(field);
|
|
85
|
+
if (currentGroup.length === 2) {
|
|
86
|
+
groups.push(currentGroup);
|
|
87
|
+
currentGroup = [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Add any remaining fields
|
|
93
|
+
if (currentGroup.length > 0) {
|
|
94
|
+
groups.push(currentGroup);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return groups;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const overviewFields = $derived(getOverviewFields());
|
|
101
|
+
const fieldGroups = $derived(groupFieldsForLayout(overviewFields));
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<!-- Readonly Overview Tab with Clean Styling -->
|
|
105
|
+
<div class="space-y-8">
|
|
106
|
+
{#if overviewFields.length > 0}
|
|
107
|
+
<div class="space-y-4">
|
|
108
|
+
<div class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-6">
|
|
109
|
+
<div class="space-y-8">
|
|
110
|
+
{#each fieldGroups as fieldGroup}
|
|
111
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
112
|
+
{#each fieldGroup as [fieldName, field]}
|
|
113
|
+
<div class={getFieldLayoutClass(field)}>
|
|
114
|
+
<FieldRenderer
|
|
115
|
+
{fieldName}
|
|
116
|
+
{field}
|
|
117
|
+
value={control[fieldName]}
|
|
118
|
+
readonly={true}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
{/each}
|
|
122
|
+
</div>
|
|
123
|
+
{/each}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
{:else}
|
|
128
|
+
<div class="text-center py-12">
|
|
129
|
+
<p class="text-gray-500 dark:text-gray-400">No overview fields available</p>
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
132
|
+
|
|
133
|
+
<!-- Control Properties -->
|
|
134
|
+
{#if control.properties && Object.keys(control.properties).length > 0}
|
|
135
|
+
<div class="space-y-4">
|
|
136
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Control Properties</h3>
|
|
137
|
+
<div class="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm hover:shadow-md transition-all duration-200 p-6">
|
|
138
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
139
|
+
{#each Object.entries(control.properties) as [key, value]}
|
|
140
|
+
<FieldRenderer
|
|
141
|
+
fieldName={key.replace(/_/g, ' ')}
|
|
142
|
+
field={null}
|
|
143
|
+
value={typeof value === 'object' ? JSON.stringify(value, null, 2) : value}
|
|
144
|
+
readonly={true}
|
|
145
|
+
/>
|
|
146
|
+
{/each}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
{/if}
|
|
151
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { TimelineItem } from '$components/version-control';
|
|
6
|
+
import type { Control } from '$lib/types';
|
|
7
|
+
import { EmptyState } from '../../ui';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
control: Control;
|
|
11
|
+
timeline?: any; // Timeline type from control.timeline
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { control, timeline }: Props = $props();
|
|
15
|
+
|
|
16
|
+
const commits = $derived(timeline?.commits || []);
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div>
|
|
20
|
+
{#if !timeline}
|
|
21
|
+
<EmptyState
|
|
22
|
+
title="No timeline data available"
|
|
23
|
+
description="Timeline information is not available for this control."
|
|
24
|
+
size="lg"
|
|
25
|
+
/>
|
|
26
|
+
{:else if commits.length > 0}
|
|
27
|
+
<div class="mb-4">
|
|
28
|
+
<div class="space-y-6">
|
|
29
|
+
{#each commits as commit, index}
|
|
30
|
+
<TimelineItem {commit} showConnector={index < commits.length - 1} />
|
|
31
|
+
{/each}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
{:else}
|
|
35
|
+
<EmptyState
|
|
36
|
+
title="No activity history found"
|
|
37
|
+
description="This control and its mapping files are new and haven't been committed to git yet."
|
|
38
|
+
size="lg"
|
|
39
|
+
/>
|
|
40
|
+
{/if}
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Lula Authors
|
|
3
|
+
|
|
4
|
+
export { default as CustomFieldsTab } from './CustomFieldsTab.svelte';
|
|
5
|
+
export { default as ImplementationTab } from './ImplementationTab.svelte';
|
|
6
|
+
export { default as MappingsTab } from './MappingsTab.svelte';
|
|
7
|
+
export { default as OverviewTab } from './OverviewTab.svelte';
|
|
8
|
+
export { default as TimelineTab } from './TimelineTab.svelte';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { processMultilineText } from './textProcessor';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { text }: Props = $props();
|
|
12
|
+
|
|
13
|
+
const sections = $derived(processMultilineText(text));
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class="space-y-3">
|
|
17
|
+
{#each sections as section}
|
|
18
|
+
{#if section.type === 'header'}
|
|
19
|
+
<h4 class="font-semibold text-gray-900 dark:text-gray-100 mt-4 first:mt-0">
|
|
20
|
+
{section.content}
|
|
21
|
+
</h4>
|
|
22
|
+
{:else if section.type === 'table' && section.data?.rows}
|
|
23
|
+
<div class="overflow-x-auto">
|
|
24
|
+
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
25
|
+
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
26
|
+
{#each section.data.rows as row, i}
|
|
27
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
28
|
+
{#each row.columns as column, j}
|
|
29
|
+
<td class="px-3 py-2 text-sm {j === 0 ? 'font-medium text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-400'}">
|
|
30
|
+
{#if column.startsWith('CCI-')}
|
|
31
|
+
<code class="px-1.5 py-0.5 text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded">
|
|
32
|
+
{column}
|
|
33
|
+
</code>
|
|
34
|
+
{:else if /^[A-Z]{2}-\d+/.test(column)}
|
|
35
|
+
<code class="px-1.5 py-0.5 text-xs bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded">
|
|
36
|
+
{column}
|
|
37
|
+
</code>
|
|
38
|
+
{:else}
|
|
39
|
+
{column}
|
|
40
|
+
{/if}
|
|
41
|
+
</td>
|
|
42
|
+
{/each}
|
|
43
|
+
</tr>
|
|
44
|
+
{/each}
|
|
45
|
+
</tbody>
|
|
46
|
+
</table>
|
|
47
|
+
</div>
|
|
48
|
+
{:else if section.type === 'list' && section.data?.items}
|
|
49
|
+
<ul class="space-y-1 ml-4">
|
|
50
|
+
{#each section.data.items as item}
|
|
51
|
+
<li class="flex items-start">
|
|
52
|
+
<span class="text-gray-400 dark:text-gray-500 mr-2 flex-shrink-0">•</span>
|
|
53
|
+
<span class="text-gray-600 dark:text-gray-400 text-sm">{item}</span>
|
|
54
|
+
</li>
|
|
55
|
+
{/each}
|
|
56
|
+
</ul>
|
|
57
|
+
{:else}
|
|
58
|
+
<p class="text-gray-600 dark:text-gray-400 text-sm whitespace-pre-wrap">
|
|
59
|
+
{section.content}
|
|
60
|
+
</p>
|
|
61
|
+
{/if}
|
|
62
|
+
{/each}
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Lula Authors
|
|
3
|
+
|
|
4
|
+
export interface ProcessedSection {
|
|
5
|
+
type: 'header' | 'paragraph' | 'table' | 'list';
|
|
6
|
+
content: string;
|
|
7
|
+
data?: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TableRow {
|
|
11
|
+
columns: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function processMultilineText(text: string): ProcessedSection[] {
|
|
15
|
+
if (!text) return [];
|
|
16
|
+
|
|
17
|
+
// First check if the entire content is a table
|
|
18
|
+
if (isEntireContentTable(text)) {
|
|
19
|
+
return processAsTable(text);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lines = text.split('\n');
|
|
23
|
+
const sections: ProcessedSection[] = [];
|
|
24
|
+
let currentSection: string[] = [];
|
|
25
|
+
let currentType: 'paragraph' | 'list' = 'paragraph';
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < lines.length; i++) {
|
|
28
|
+
const line = lines[i];
|
|
29
|
+
const trimmedLine = line.trim();
|
|
30
|
+
|
|
31
|
+
// Check for empty lines
|
|
32
|
+
if (!trimmedLine) {
|
|
33
|
+
// Process accumulated section if any
|
|
34
|
+
if (currentSection.length > 0) {
|
|
35
|
+
flushSection();
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check for headers (lines ending with colon and followed by content)
|
|
41
|
+
if (trimmedLine.endsWith(':') && i < lines.length - 1) {
|
|
42
|
+
const nextLine = lines[i + 1]?.trim();
|
|
43
|
+
// Only treat as header if next line has content or is followed by content
|
|
44
|
+
if (nextLine || (i + 2 < lines.length && lines[i + 2]?.trim())) {
|
|
45
|
+
// Flush any accumulated section
|
|
46
|
+
if (currentSection.length > 0) {
|
|
47
|
+
flushSection();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add header
|
|
51
|
+
sections.push({
|
|
52
|
+
type: 'header',
|
|
53
|
+
content: trimmedLine.slice(0, -1) // Remove the colon
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check for list items
|
|
60
|
+
if (isListItem(trimmedLine)) {
|
|
61
|
+
if (currentType !== 'list') {
|
|
62
|
+
// Flush previous section if not list
|
|
63
|
+
if (currentSection.length > 0) {
|
|
64
|
+
flushSection();
|
|
65
|
+
}
|
|
66
|
+
currentType = 'list';
|
|
67
|
+
}
|
|
68
|
+
currentSection.push(trimmedLine);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Default to paragraph
|
|
73
|
+
if (currentType === 'list') {
|
|
74
|
+
// Flush previous section if switching from list to paragraph
|
|
75
|
+
if (currentSection.length > 0) {
|
|
76
|
+
flushSection();
|
|
77
|
+
}
|
|
78
|
+
currentType = 'paragraph';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
currentSection.push(line); // Preserve original line formatting for paragraphs
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Flush any remaining section
|
|
85
|
+
if (currentSection.length > 0) {
|
|
86
|
+
flushSection();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function flushSection() {
|
|
90
|
+
if (currentType === 'list') {
|
|
91
|
+
const items = currentSection.map(line => {
|
|
92
|
+
// Remove common list prefixes
|
|
93
|
+
return line.replace(/^[-*•]\s*/, '')
|
|
94
|
+
.replace(/^\d+\.\s*/, '')
|
|
95
|
+
.replace(/^[a-zA-Z]\.\s*/, '');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
sections.push({
|
|
99
|
+
type: 'list',
|
|
100
|
+
content: currentSection.join('\n'),
|
|
101
|
+
data: { items }
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
sections.push({
|
|
105
|
+
type: 'paragraph',
|
|
106
|
+
content: currentSection.join('\n').trim()
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
currentSection = [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return sections;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isEntireContentTable(text: string): boolean {
|
|
117
|
+
const lines = text.split('\n').filter(line => line.trim());
|
|
118
|
+
if (lines.length === 0) return false;
|
|
119
|
+
|
|
120
|
+
// Check if ALL non-empty lines are table rows
|
|
121
|
+
return lines.every(line => isTableRow(line.trim()));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function processAsTable(text: string): ProcessedSection[] {
|
|
125
|
+
const lines = text.split('\n').filter(line => line.trim());
|
|
126
|
+
const rows: TableRow[] = lines.map(line => ({
|
|
127
|
+
columns: line.split(',').map(col => col.trim())
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
return [{
|
|
131
|
+
type: 'table',
|
|
132
|
+
content: text,
|
|
133
|
+
data: { rows }
|
|
134
|
+
}];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isTableRow(line: string): boolean {
|
|
138
|
+
// Check if line contains comma-separated values
|
|
139
|
+
// Must have at least 2 commas and consistent structure
|
|
140
|
+
const parts = line.split(',');
|
|
141
|
+
if (parts.length < 2) return false;
|
|
142
|
+
|
|
143
|
+
// Common patterns for table data:
|
|
144
|
+
// - Control IDs (AC-10.1)
|
|
145
|
+
// - CCI codes (CCI-000054)
|
|
146
|
+
// - Short descriptive text
|
|
147
|
+
const hasControlPattern = /^[A-Z]{2}-\d+(\.\d+)?/.test(parts[0].trim());
|
|
148
|
+
const hasCCIPattern = parts.some(part => /^CCI-\d+/.test(part.trim()));
|
|
149
|
+
|
|
150
|
+
// If it looks like structured data with consistent separators
|
|
151
|
+
return hasControlPattern || hasCCIPattern ||
|
|
152
|
+
(parts.length >= 2 && parts.every(p => p.trim().length > 0 && p.trim().length < 100));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isListItem(line: string): boolean {
|
|
156
|
+
// Check for common list patterns
|
|
157
|
+
return /^[-*•]\s+/.test(line) || // Bullet points
|
|
158
|
+
/^\d+\.\s+/.test(line) || // Numbered lists
|
|
159
|
+
/^[a-zA-Z]\.\s+/.test(line) || // Letter lists
|
|
160
|
+
/^\[SELECT FROM:/.test(line) || // Special NIST format
|
|
161
|
+
/^Examine:/.test(line) ||
|
|
162
|
+
/^Interview:/.test(line) ||
|
|
163
|
+
/^Test:/.test(line);
|
|
164
|
+
}
|