@blokkli/editor 2.0.0-alpha.43 → 2.0.0-alpha.44
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/dist/module.json +1 -1
- package/dist/module.mjs +3 -4
- package/dist/modules/agent/runtime/app/composables/agentProvider.js +28 -19
- package/dist/modules/agent/runtime/app/tools/check_readability/index.js +1 -0
- package/dist/modules/agent/runtime/app/tools/delegate_text_rewrite/Component.d.vue.ts +2 -0
- package/dist/modules/agent/runtime/app/tools/delegate_text_rewrite/Component.vue +7 -3
- package/dist/modules/agent/runtime/app/tools/delegate_text_rewrite/Component.vue.d.ts +2 -0
- package/dist/modules/agent/runtime/app/tools/helpers.js +5 -3
- package/dist/modules/agent/runtime/server/Session.d.ts +2 -0
- package/dist/modules/agent/runtime/server/Session.js +17 -0
- package/dist/modules/agent/runtime/server/agent.js +2 -1
- package/dist/modules/agent/runtime/shared/types.d.ts +1 -0
- package/dist/modules/agent/runtime/shared/types.js +2 -1
- package/dist/runtime/components/BlokkliProvider.vue +14 -13
- package/dist/runtime/editor/components/Toolbar/index.vue +1 -2
- package/dist/runtime/editor/composables/useStickyToolbar.d.ts +1 -1
- package/dist/runtime/editor/composables/useStickyToolbar.js +25 -7
- package/dist/runtime/editor/css/output.css +1 -1
- package/dist/runtime/editor/features/analyze/Main.d.vue.ts +6 -0
- package/dist/runtime/editor/features/analyze/Main.vue +26 -1
- package/dist/runtime/editor/features/analyze/Main.vue.d.ts +6 -0
- package/dist/runtime/editor/features/analyze/Renderer/index.d.vue.ts +2 -0
- package/dist/runtime/editor/features/analyze/Renderer/index.vue +86 -15
- package/dist/runtime/editor/features/analyze/Renderer/index.vue.d.ts +2 -0
- package/dist/runtime/editor/features/analyze/analyzers/altText.d.ts +2 -0
- package/dist/runtime/editor/features/analyze/analyzers/altText.js +60 -0
- package/dist/runtime/editor/features/analyze/analyzers/headingStructure.d.ts +2 -0
- package/dist/runtime/editor/features/analyze/analyzers/headingStructure.js +141 -0
- package/dist/runtime/editor/features/analyze/analyzers/index.d.ts +5 -1
- package/dist/runtime/editor/features/analyze/analyzers/index.js +11 -1
- package/dist/runtime/editor/features/analyze/analyzers/readability.js +50 -16
- package/dist/runtime/editor/features/analyze/analyzers/types.d.ts +3 -2
- package/dist/runtime/editor/features/analyze/index.vue +12 -0
- package/dist/runtime/editor/features/analyze/readability/builtinAnalyzer.js +38 -22
- package/dist/runtime/editor/features/analyze/readability/types.d.ts +18 -3
- package/dist/runtime/editor/features/dragging-overlay/DragItems/index.d.vue.ts +1 -0
- package/dist/runtime/editor/features/dragging-overlay/DragItems/index.vue +102 -2
- package/dist/runtime/editor/features/dragging-overlay/DragItems/index.vue.d.ts +1 -0
- package/dist/runtime/editor/features/dragging-overlay/Renderer/index.d.vue.ts +1 -0
- package/dist/runtime/editor/features/dragging-overlay/Renderer/index.vue +16 -1
- package/dist/runtime/editor/features/dragging-overlay/Renderer/index.vue.d.ts +1 -0
- package/dist/runtime/editor/features/dragging-overlay/index.vue +2 -1
- package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/ChunkOverlay.d.vue.ts +8 -0
- package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/ChunkOverlay.vue +135 -0
- package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/ChunkOverlay.vue.d.ts +8 -0
- package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/index.d.vue.ts +7 -0
- package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/index.vue +187 -0
- package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/index.vue.d.ts +7 -0
- package/dist/runtime/editor/features/editable-field/Overlay/index.vue +23 -0
- package/dist/runtime/editor/features/options/index.vue +4 -3
- package/dist/runtime/editor/features/search/Overlay/Results/Content/index.vue +7 -11
- package/dist/runtime/editor/features/search/Overlay/Results/Page/index.vue +11 -13
- package/dist/runtime/editor/features/selection/Renderer/index.vue +2 -0
- package/dist/runtime/editor/features/translations/index.vue +1 -1
- package/dist/runtime/editor/providers/analyze.js +1 -1
- package/dist/runtime/editor/providers/readability.js +16 -20
- package/dist/runtime/editor/translations/de.json +113 -1
- package/dist/runtime/editor/translations/fr.json +113 -1
- package/dist/runtime/editor/translations/gsw_CH.json +113 -1
- package/dist/runtime/editor/translations/it.json +113 -1
- package/package.json +1 -1
|
@@ -6,12 +6,18 @@ type __VLS_Props = {
|
|
|
6
6
|
};
|
|
7
7
|
type __VLS_ModelProps = {
|
|
8
8
|
modelValue?: boolean;
|
|
9
|
+
'issueCount'?: number;
|
|
10
|
+
'hasViolation'?: boolean;
|
|
9
11
|
};
|
|
10
12
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
11
13
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
14
|
"update:modelValue": (value: boolean) => any;
|
|
15
|
+
"update:issueCount": (value: number) => any;
|
|
16
|
+
"update:hasViolation": (value: boolean) => any;
|
|
13
17
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
14
18
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
19
|
+
"onUpdate:issueCount"?: ((value: number) => any) | undefined;
|
|
20
|
+
"onUpdate:hasViolation"?: ((value: boolean) => any) | undefined;
|
|
15
21
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
22
|
declare const _default: typeof __VLS_export;
|
|
17
23
|
export default _default;
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
<FormToggle
|
|
25
25
|
v-model="keepVisible"
|
|
26
26
|
:label="$t('analyzeKeepVisible', 'Keep results visible')"
|
|
27
|
+
:description="
|
|
28
|
+
$t(
|
|
29
|
+
'analyzeKeepVisibleDescription',
|
|
30
|
+
'When enabled, analysis results remain highlighted on the page even when the analyze panel is closed.'
|
|
31
|
+
)
|
|
32
|
+
"
|
|
27
33
|
/>
|
|
28
34
|
|
|
29
35
|
<div v-if="analyzerStatuses.length > 1" class="bk-analyze-statuses">
|
|
@@ -67,6 +73,7 @@
|
|
|
67
73
|
:is-stale
|
|
68
74
|
:manual-analyzer-ids
|
|
69
75
|
:is-running
|
|
76
|
+
:is-shown
|
|
70
77
|
/>
|
|
71
78
|
</template>
|
|
72
79
|
|
|
@@ -101,6 +108,8 @@ const refreshKey = computed(() => {
|
|
|
101
108
|
return `dom:${dom.settleKey.value}_directive:${directive.settleKey.value}_state:${state.refreshKey.value}`;
|
|
102
109
|
});
|
|
103
110
|
const isRunning = defineModel({ type: Boolean, ...{ default: false } });
|
|
111
|
+
const issueCount = defineModel("issueCount", { type: Number, ...{ default: 0 } });
|
|
112
|
+
const hasViolation = defineModel("hasViolation", { type: Boolean, ...{ default: false } });
|
|
104
113
|
let currentAbortController = null;
|
|
105
114
|
const hasRunOnce = useState(() => false);
|
|
106
115
|
const continuousResults = useState(
|
|
@@ -172,6 +181,22 @@ const results = computed(() => {
|
|
|
172
181
|
}
|
|
173
182
|
return allResults.value.filter((v) => v.category === selectedCategory.value);
|
|
174
183
|
});
|
|
184
|
+
watch(
|
|
185
|
+
allResults,
|
|
186
|
+
(v) => {
|
|
187
|
+
let count = 0;
|
|
188
|
+
for (const r of v) {
|
|
189
|
+
if (r.status === "violation" || r.status === "incomplete") {
|
|
190
|
+
for (const node of r.nodes) {
|
|
191
|
+
count += node.targets.length;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
issueCount.value = count;
|
|
196
|
+
hasViolation.value = v.some((r) => r.status === "violation");
|
|
197
|
+
},
|
|
198
|
+
{ immediate: true }
|
|
199
|
+
);
|
|
175
200
|
const isStale = computed(() => lastRunKey.value !== state.refreshKey.value);
|
|
176
201
|
const buttonDisabled = computed(() => {
|
|
177
202
|
if (isRunning.value) {
|
|
@@ -203,7 +228,7 @@ const analyzerStatuses = computed(() => {
|
|
|
203
228
|
}
|
|
204
229
|
return props.analyze.analyzers.value.map((analyzer) => {
|
|
205
230
|
const status = analyzer.continuous ? $t("analyzeStatusUpToDate", "Up-to-date") : isStale.value ? $t("analyzeStatusStale", "Stale") : $t("analyzeStatusUpToDate", "Up-to-date");
|
|
206
|
-
const title = typeof analyzer.label === "function" ? analyzer.label(ui.interfaceLanguage.value) : analyzer.label;
|
|
231
|
+
const title = typeof analyzer.label === "function" ? analyzer.label(ui.interfaceLanguage.value, $t) : analyzer.label;
|
|
207
232
|
return {
|
|
208
233
|
id: analyzer.id,
|
|
209
234
|
title: title ?? analyzer.id,
|
|
@@ -6,12 +6,18 @@ type __VLS_Props = {
|
|
|
6
6
|
};
|
|
7
7
|
type __VLS_ModelProps = {
|
|
8
8
|
modelValue?: boolean;
|
|
9
|
+
'issueCount'?: number;
|
|
10
|
+
'hasViolation'?: boolean;
|
|
9
11
|
};
|
|
10
12
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
11
13
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
14
|
"update:modelValue": (value: boolean) => any;
|
|
15
|
+
"update:issueCount": (value: number) => any;
|
|
16
|
+
"update:hasViolation": (value: boolean) => any;
|
|
13
17
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
14
18
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
19
|
+
"onUpdate:issueCount"?: ((value: number) => any) | undefined;
|
|
20
|
+
"onUpdate:hasViolation"?: ((value: boolean) => any) | undefined;
|
|
15
21
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
22
|
declare const _default: typeof __VLS_export;
|
|
17
23
|
export default _default;
|
|
@@ -6,6 +6,7 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
6
6
|
isStale: boolean;
|
|
7
7
|
isRunning: boolean;
|
|
8
8
|
manualAnalyzerIds: Set<string>;
|
|
9
|
+
isShown: boolean;
|
|
9
10
|
} & {
|
|
10
11
|
modelValue?: string;
|
|
11
12
|
}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
@@ -15,6 +16,7 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
15
16
|
isStale: boolean;
|
|
16
17
|
isRunning: boolean;
|
|
17
18
|
manualAnalyzerIds: Set<string>;
|
|
19
|
+
isShown: boolean;
|
|
18
20
|
} & {
|
|
19
21
|
modelValue?: string;
|
|
20
22
|
}> & Readonly<{
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<Teleport to="#bk-canvas-overlay">
|
|
3
|
+
<button
|
|
4
|
+
v-if="tooltipData"
|
|
5
|
+
v-show="showTooltip"
|
|
6
|
+
class="bk bk-analyze-tooltip bk-control"
|
|
7
|
+
:class="'bk-is-' + tooltipData.status"
|
|
8
|
+
:style="{
|
|
9
|
+
transform: `translate(${tooltipData.x}px, ${tooltipData.y}px)`
|
|
10
|
+
}"
|
|
11
|
+
@click.prevent="onTooltipClick"
|
|
12
|
+
>
|
|
13
|
+
<Icon name="bk_mdi_speed" />
|
|
14
|
+
<span>{{ tooltipData.title }}</span>
|
|
15
|
+
<span v-if="tooltipData.scoreText" class="bk-analyze-tooltip-score">{{
|
|
16
|
+
tooltipData.scoreText
|
|
17
|
+
}}</span>
|
|
18
|
+
</button>
|
|
19
|
+
</Teleport>
|
|
3
20
|
</template>
|
|
4
21
|
|
|
5
22
|
<script setup>
|
|
6
|
-
import { useBlokkli, computed, watch } from "#imports";
|
|
23
|
+
import { useBlokkli, computed, watch, ref } from "#imports";
|
|
7
24
|
import {
|
|
8
25
|
setBuffersAndAttributes,
|
|
9
26
|
drawBufferInfo,
|
|
@@ -14,13 +31,28 @@ import fs from "./fragment.glsl?raw";
|
|
|
14
31
|
import { RectangleBufferCollector } from "#blokkli/editor/helpers/webgl";
|
|
15
32
|
import { toShaderColor } from "#blokkli/editor/helpers/color";
|
|
16
33
|
import { defineRenderer, onBlokkliEvent } from "#blokkli/editor/composables";
|
|
34
|
+
import { Icon } from "#blokkli/editor/components";
|
|
17
35
|
const props = defineProps({
|
|
18
36
|
results: { type: Array, required: true },
|
|
19
37
|
isStale: { type: Boolean, required: true },
|
|
20
38
|
isRunning: { type: Boolean, required: true },
|
|
21
|
-
manualAnalyzerIds: { type: Set, required: true }
|
|
39
|
+
manualAnalyzerIds: { type: Set, required: true },
|
|
40
|
+
isShown: { type: Boolean, required: true }
|
|
41
|
+
});
|
|
42
|
+
const {
|
|
43
|
+
animation,
|
|
44
|
+
ui,
|
|
45
|
+
theme,
|
|
46
|
+
selection,
|
|
47
|
+
element,
|
|
48
|
+
dom,
|
|
49
|
+
blocks,
|
|
50
|
+
eventBus,
|
|
51
|
+
readability
|
|
52
|
+
} = useBlokkli();
|
|
53
|
+
const showTooltip = computed(() => {
|
|
54
|
+
return !ui.isChangingOptions.value && !selection.isMultiSelecting.value && !selection.activeEditableLabel.value;
|
|
22
55
|
});
|
|
23
|
-
const { animation, ui, theme, selection, element, dom, blocks } = useBlokkli();
|
|
24
56
|
const activeId = defineModel({ type: String, ...{
|
|
25
57
|
default: ""
|
|
26
58
|
} });
|
|
@@ -72,7 +104,8 @@ const nodes = computed(() => {
|
|
|
72
104
|
index: target.globalIndex,
|
|
73
105
|
title: result.title,
|
|
74
106
|
status: result.status,
|
|
75
|
-
plugin: result.plugin
|
|
107
|
+
plugin: result.plugin,
|
|
108
|
+
score: node.score
|
|
76
109
|
});
|
|
77
110
|
}
|
|
78
111
|
}
|
|
@@ -101,6 +134,47 @@ const activeRectId = computed(() => {
|
|
|
101
134
|
}
|
|
102
135
|
return -1;
|
|
103
136
|
});
|
|
137
|
+
const hoveredNode = ref(null);
|
|
138
|
+
const tooltipData = computed(() => {
|
|
139
|
+
const node = hoveredNode.value;
|
|
140
|
+
if (!node) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const rect = collector.rectCache.get(node.element);
|
|
144
|
+
if (!rect) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const scale = ui.artboardScale.value;
|
|
148
|
+
const offset = ui.artboardOffset.value;
|
|
149
|
+
const x = rect.x * scale + offset.x;
|
|
150
|
+
const y = rect.y * scale + offset.y;
|
|
151
|
+
let scoreText = "";
|
|
152
|
+
if (node.score != null) {
|
|
153
|
+
const label = readability.analyzer.value.scoreLabel;
|
|
154
|
+
scoreText = `${label} ${readability.formatScore(node.score)}`;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
x,
|
|
158
|
+
y,
|
|
159
|
+
title: node.title,
|
|
160
|
+
status: node.status,
|
|
161
|
+
scoreText
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
function onTooltipClick() {
|
|
165
|
+
const node = hoveredNode.value;
|
|
166
|
+
if (!node) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const id = node.id + "_____" + node.index;
|
|
170
|
+
if (activeId.value === id) {
|
|
171
|
+
activeId.value = "";
|
|
172
|
+
} else {
|
|
173
|
+
activeId.value = id;
|
|
174
|
+
eventBus.emit("sidebar:open", "analyze");
|
|
175
|
+
}
|
|
176
|
+
hoveredNode.value = null;
|
|
177
|
+
}
|
|
104
178
|
class AnalyzeRectangleBufferCollector extends RectangleBufferCollector {
|
|
105
179
|
prevKey = "";
|
|
106
180
|
rectCache = /* @__PURE__ */ new Map();
|
|
@@ -165,7 +239,7 @@ const { collector } = defineRenderer("analyze-overlay", {
|
|
|
165
239
|
zIndex: 500,
|
|
166
240
|
collector: () => new AnalyzeRectangleBufferCollector(),
|
|
167
241
|
program: () => ({ shaders: [vs, fs] }),
|
|
168
|
-
enabled: () => !selection.isMultiSelecting.value && !selection.isDragging.value && !ui.isChangingOptions.value,
|
|
242
|
+
enabled: () => !selection.isMultiSelecting.value && !selection.isDragging.value && !ui.isChangingOptions.value && !selection.activeEditableLabel.value,
|
|
169
243
|
render: (_ctx, gl, program) => {
|
|
170
244
|
gl.useProgram(program.program);
|
|
171
245
|
const { info } = collector.getBufferInfo(gl);
|
|
@@ -227,9 +301,9 @@ watch(
|
|
|
227
301
|
collector.reset();
|
|
228
302
|
}
|
|
229
303
|
);
|
|
230
|
-
onBlokkliEvent("
|
|
231
|
-
const artboardX = (e.
|
|
232
|
-
const artboardY = (e.
|
|
304
|
+
onBlokkliEvent("canvas:draw", (e) => {
|
|
305
|
+
const artboardX = (e.mouseX - e.artboardOffset.x) / e.artboardScale;
|
|
306
|
+
const artboardY = (e.mouseY - e.artboardOffset.y) / e.artboardScale;
|
|
233
307
|
for (let i = 0; i < nodes.value.length; i++) {
|
|
234
308
|
const node = nodes.value[i];
|
|
235
309
|
const rect = collector.rectCache.get(node.element);
|
|
@@ -237,17 +311,14 @@ onBlokkliEvent("mouse:up", (e) => {
|
|
|
237
311
|
continue;
|
|
238
312
|
}
|
|
239
313
|
if (artboardX >= rect.x && artboardX <= rect.x + rect.width && artboardY >= rect.y && artboardY <= rect.y + rect.height) {
|
|
240
|
-
|
|
241
|
-
if (activeId.value === id) {
|
|
242
|
-
activeId.value = "";
|
|
243
|
-
} else {
|
|
244
|
-
activeId.value = id;
|
|
245
|
-
}
|
|
314
|
+
hoveredNode.value = node;
|
|
246
315
|
return;
|
|
247
316
|
}
|
|
248
317
|
}
|
|
318
|
+
hoveredNode.value = null;
|
|
249
319
|
});
|
|
250
320
|
onBlokkliEvent("window:clickAway", () => {
|
|
321
|
+
hoveredNode.value = null;
|
|
251
322
|
activeId.value = "";
|
|
252
323
|
});
|
|
253
324
|
</script>
|
|
@@ -6,6 +6,7 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
6
6
|
isStale: boolean;
|
|
7
7
|
isRunning: boolean;
|
|
8
8
|
manualAnalyzerIds: Set<string>;
|
|
9
|
+
isShown: boolean;
|
|
9
10
|
} & {
|
|
10
11
|
modelValue?: string;
|
|
11
12
|
}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
@@ -15,6 +16,7 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
15
16
|
isStale: boolean;
|
|
16
17
|
isRunning: boolean;
|
|
17
18
|
manualAnalyzerIds: Set<string>;
|
|
19
|
+
isShown: boolean;
|
|
18
20
|
} & {
|
|
19
21
|
modelValue?: string;
|
|
20
22
|
}> & Readonly<{
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineAnalyzer } from "#blokkli/analyzer";
|
|
2
|
+
export default defineAnalyzer(() => {
|
|
3
|
+
return {
|
|
4
|
+
id: "blokkli:image-alt-text",
|
|
5
|
+
label: (_langcode, $t) => $t("analyzeAltTextLabel", "Image Alt Texts"),
|
|
6
|
+
continuous: true,
|
|
7
|
+
run: async (context) => {
|
|
8
|
+
const $t = context.$t;
|
|
9
|
+
const images = [
|
|
10
|
+
...context.providerRootElement.querySelectorAll("img")
|
|
11
|
+
].map((img) => {
|
|
12
|
+
const alt = img.getAttribute("alt");
|
|
13
|
+
return {
|
|
14
|
+
element: img,
|
|
15
|
+
hasAlt: alt !== null && alt.trim() !== "",
|
|
16
|
+
alt: alt || "",
|
|
17
|
+
src: img.src.substring(0, 50)
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
const withAlt = images.filter((v) => v.hasAlt);
|
|
21
|
+
const withoutAlt = images.filter((v) => !v.hasAlt);
|
|
22
|
+
const results = [];
|
|
23
|
+
results.push({
|
|
24
|
+
id: "blokkli:image-alt-text:valid",
|
|
25
|
+
title: $t("analyzeAltTextValid", "Images with alt text"),
|
|
26
|
+
category: "accessibility",
|
|
27
|
+
description: $t(
|
|
28
|
+
"analyzeAltTextValidDescription",
|
|
29
|
+
"Images have an alt text."
|
|
30
|
+
),
|
|
31
|
+
status: "pass",
|
|
32
|
+
nodes: withAlt.map((img) => ({
|
|
33
|
+
targets: img.element
|
|
34
|
+
}))
|
|
35
|
+
});
|
|
36
|
+
if (withoutAlt.length > 0) {
|
|
37
|
+
results.push({
|
|
38
|
+
id: "blokkli:image-alt-text:missing",
|
|
39
|
+
title: $t("analyzeAltTextMissing", "Images without alt text"),
|
|
40
|
+
category: "accessibility",
|
|
41
|
+
description: $t(
|
|
42
|
+
"analyzeAltTextMissingDescription",
|
|
43
|
+
"Images are missing alt text. Alt texts are important for accessibility and SEO."
|
|
44
|
+
),
|
|
45
|
+
status: "violation",
|
|
46
|
+
impact: "serious",
|
|
47
|
+
nodes: withoutAlt.map((img) => ({
|
|
48
|
+
description: $t(
|
|
49
|
+
"analyzeAltTextMissingNode",
|
|
50
|
+
"Image without alt text"
|
|
51
|
+
),
|
|
52
|
+
impact: "serious",
|
|
53
|
+
targets: img.element
|
|
54
|
+
}))
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { falsy } from "#blokkli/helpers";
|
|
2
|
+
import { defineAnalyzer } from "./defineAnalyzer.js";
|
|
3
|
+
export default defineAnalyzer(() => {
|
|
4
|
+
return {
|
|
5
|
+
id: "blokkli:heading-structure",
|
|
6
|
+
label: (_langcode, $t) => $t("analyzeHeadingStructureLabel", "Heading Structure"),
|
|
7
|
+
continuous: true,
|
|
8
|
+
run: async (context) => {
|
|
9
|
+
const $t = context.$t;
|
|
10
|
+
const results = [];
|
|
11
|
+
const allHeadings = [
|
|
12
|
+
...context.providerRootElement.querySelectorAll(
|
|
13
|
+
"h1, h2, h3, h4, h5, h6"
|
|
14
|
+
)
|
|
15
|
+
].map((heading) => {
|
|
16
|
+
if (heading instanceof HTMLElement) {
|
|
17
|
+
const level = parseInt(heading.tagName.substring(1));
|
|
18
|
+
return {
|
|
19
|
+
element: heading,
|
|
20
|
+
level,
|
|
21
|
+
text: heading.textContent?.trim() || ""
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}).filter(falsy);
|
|
25
|
+
const h1Elements = allHeadings.filter((h) => h.level === 1);
|
|
26
|
+
if (h1Elements.length > 1) {
|
|
27
|
+
results.push({
|
|
28
|
+
id: "blokkli:heading-structure:multiple-h1",
|
|
29
|
+
title: $t("analyzeHeadingMultipleH1", "Multiple H1 headings"),
|
|
30
|
+
category: "seo",
|
|
31
|
+
description: $t(
|
|
32
|
+
"analyzeHeadingMultipleH1Description",
|
|
33
|
+
"The page contains multiple H1 headings. There should only be one H1 heading per page."
|
|
34
|
+
),
|
|
35
|
+
status: "violation",
|
|
36
|
+
impact: "serious",
|
|
37
|
+
nodes: h1Elements.map((h) => ({
|
|
38
|
+
description: h.text,
|
|
39
|
+
targets: h.element
|
|
40
|
+
}))
|
|
41
|
+
});
|
|
42
|
+
} else if (h1Elements.length === 1) {
|
|
43
|
+
results.push({
|
|
44
|
+
id: "blokkli:heading-structure:single-h1",
|
|
45
|
+
title: $t("analyzeHeadingSingleH1", "Single H1 heading"),
|
|
46
|
+
category: "seo",
|
|
47
|
+
description: $t(
|
|
48
|
+
"analyzeHeadingSingleH1Description",
|
|
49
|
+
"The page has exactly one H1 heading."
|
|
50
|
+
),
|
|
51
|
+
status: "pass",
|
|
52
|
+
nodes: h1Elements.map((h) => ({
|
|
53
|
+
description: h.text,
|
|
54
|
+
targets: h.element
|
|
55
|
+
}))
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const orderIssues = [];
|
|
59
|
+
for (let i = 1; i < allHeadings.length; i++) {
|
|
60
|
+
const previous = allHeadings[i - 1];
|
|
61
|
+
const current = allHeadings[i];
|
|
62
|
+
if (!previous || !current) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (current.level > previous.level + 1) {
|
|
66
|
+
orderIssues.push({ current, previous });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (orderIssues.length > 0) {
|
|
70
|
+
results.push({
|
|
71
|
+
id: "blokkli:heading-structure:skipped-levels",
|
|
72
|
+
title: $t("analyzeHeadingSkippedLevels", "Skipped heading levels"),
|
|
73
|
+
category: "seo",
|
|
74
|
+
description: $t(
|
|
75
|
+
"analyzeHeadingSkippedLevelsDescription",
|
|
76
|
+
"Headings skip a level. The heading hierarchy should not skip levels (e.g. do not jump from H2 to H4)."
|
|
77
|
+
),
|
|
78
|
+
status: "violation",
|
|
79
|
+
impact: "moderate",
|
|
80
|
+
nodes: orderIssues.map(({ current, previous }) => ({
|
|
81
|
+
description: `${previous.element.tagName} \u2192 ${current.element.tagName}: "${current.text}"`,
|
|
82
|
+
impact: "moderate",
|
|
83
|
+
targets: current.element
|
|
84
|
+
}))
|
|
85
|
+
});
|
|
86
|
+
} else if (allHeadings.length > 1) {
|
|
87
|
+
results.push({
|
|
88
|
+
id: "blokkli:heading-structure:no-skipped-levels",
|
|
89
|
+
title: $t(
|
|
90
|
+
"analyzeHeadingNoSkippedLevels",
|
|
91
|
+
"No skipped heading levels"
|
|
92
|
+
),
|
|
93
|
+
category: "seo",
|
|
94
|
+
description: $t(
|
|
95
|
+
"analyzeHeadingNoSkippedLevelsDescription",
|
|
96
|
+
"The heading hierarchy does not skip any levels."
|
|
97
|
+
),
|
|
98
|
+
status: "pass",
|
|
99
|
+
nodes: allHeadings.map((h) => ({
|
|
100
|
+
description: `${h.element.tagName}: ${h.text}`,
|
|
101
|
+
targets: h.element
|
|
102
|
+
}))
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const h2Elements = allHeadings.filter((h) => h.level === 2);
|
|
106
|
+
if (h2Elements.length === 0) {
|
|
107
|
+
results.push({
|
|
108
|
+
id: "blokkli:heading-structure:no-h2",
|
|
109
|
+
title: $t("analyzeHeadingNoH2", "Missing H2 headings"),
|
|
110
|
+
category: "seo",
|
|
111
|
+
description: $t(
|
|
112
|
+
"analyzeHeadingNoH2Description",
|
|
113
|
+
"The page contains no H2 headings. A clear heading structure is important for SEO and accessibility."
|
|
114
|
+
),
|
|
115
|
+
status: "violation",
|
|
116
|
+
impact: "moderate",
|
|
117
|
+
nodes: {
|
|
118
|
+
description: $t("analyzeHeadingNoH2Found", "No H2 headings found"),
|
|
119
|
+
targets: context.providerRootElement
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
results.push({
|
|
124
|
+
id: "blokkli:heading-structure:has-h2",
|
|
125
|
+
title: $t("analyzeHeadingHasH2", "H2 headings present"),
|
|
126
|
+
category: "seo",
|
|
127
|
+
description: $t(
|
|
128
|
+
"analyzeHeadingHasH2Description",
|
|
129
|
+
"The page contains H2 headings for a clear content structure."
|
|
130
|
+
),
|
|
131
|
+
status: "pass",
|
|
132
|
+
nodes: h2Elements.map((h) => ({
|
|
133
|
+
description: h.text,
|
|
134
|
+
targets: h.element
|
|
135
|
+
}))
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
});
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import { defineAnalyzer } from './defineAnalyzer.js';
|
|
2
|
-
|
|
2
|
+
import readabilityAnalyzer from './readability.js';
|
|
3
|
+
import headingStructureAnalyzer from './headingStructure.js';
|
|
4
|
+
import altTextAnalyzer from './altText.js';
|
|
5
|
+
import axeAnalyzer from './axe.js';
|
|
6
|
+
export { defineAnalyzer, readabilityAnalyzer, headingStructureAnalyzer, altTextAnalyzer, axeAnalyzer, };
|
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
import { defineAnalyzer } from "./defineAnalyzer.js";
|
|
2
|
-
|
|
2
|
+
import readabilityAnalyzer from "./readability.js";
|
|
3
|
+
import headingStructureAnalyzer from "./headingStructure.js";
|
|
4
|
+
import altTextAnalyzer from "./altText.js";
|
|
5
|
+
import axeAnalyzer from "./axe.js";
|
|
6
|
+
export {
|
|
7
|
+
defineAnalyzer,
|
|
8
|
+
readabilityAnalyzer,
|
|
9
|
+
headingStructureAnalyzer,
|
|
10
|
+
altTextAnalyzer,
|
|
11
|
+
axeAnalyzer
|
|
12
|
+
};
|
|
@@ -13,14 +13,23 @@ function summarizeImpact(nodes) {
|
|
|
13
13
|
function format(n, d = 1) {
|
|
14
14
|
return typeof n === "number" && Number.isFinite(n) ? n.toFixed(d) : "\u2014";
|
|
15
15
|
}
|
|
16
|
-
function buildDescription(score, scoreLabel, lang, $t) {
|
|
16
|
+
function buildDescription(score, scoreLabel, lang, band, $t) {
|
|
17
17
|
const parts = [];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"@lang
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
if (band === "hard") {
|
|
19
|
+
parts.push(
|
|
20
|
+
$t("analyzerReadabiliyHardToRead", `Hard to read (@lang).`).replace(
|
|
21
|
+
"@lang",
|
|
22
|
+
lang.toUpperCase()
|
|
23
|
+
)
|
|
24
|
+
);
|
|
25
|
+
} else {
|
|
26
|
+
parts.push(
|
|
27
|
+
$t(
|
|
28
|
+
"analyzerReadabiliyCouldBeSimpler",
|
|
29
|
+
`Could be simpler (@lang).`
|
|
30
|
+
).replace("@lang", lang.toUpperCase())
|
|
31
|
+
);
|
|
32
|
+
}
|
|
24
33
|
parts.push(`${scoreLabel} ${format(score)}`);
|
|
25
34
|
parts.push(
|
|
26
35
|
$t(
|
|
@@ -33,7 +42,8 @@ function buildDescription(score, scoreLabel, lang, $t) {
|
|
|
33
42
|
async function analyzeViaProvider(readabilityProvider, langcode, $t) {
|
|
34
43
|
const result = await readabilityProvider.analyzeAllFields();
|
|
35
44
|
const analyzer = readabilityProvider.analyzer.value;
|
|
36
|
-
const
|
|
45
|
+
const hardNodes = [];
|
|
46
|
+
const okNodes = [];
|
|
37
47
|
for (const [key, fieldResult] of Object.entries(result)) {
|
|
38
48
|
const separatorIndex = key.indexOf("/");
|
|
39
49
|
const uuid = key.slice(0, separatorIndex);
|
|
@@ -42,26 +52,34 @@ async function analyzeViaProvider(readabilityProvider, langcode, $t) {
|
|
|
42
52
|
const fieldTextElements = fieldElement ? collectTextElements(fieldElement) : [];
|
|
43
53
|
for (let i = 0; i < fieldResult.chunks.length; i++) {
|
|
44
54
|
const chunk = fieldResult.chunks[i];
|
|
45
|
-
if (chunk.band
|
|
55
|
+
if (chunk.band === "easy" || chunk.band === null || chunk.score === null)
|
|
56
|
+
continue;
|
|
46
57
|
const targets = [];
|
|
47
58
|
const textElement = fieldTextElements[i];
|
|
48
59
|
if (textElement) {
|
|
49
60
|
targets.push(textElement.element);
|
|
50
61
|
}
|
|
51
|
-
|
|
62
|
+
const node = {
|
|
52
63
|
description: buildDescription(
|
|
53
64
|
chunk.score,
|
|
54
65
|
analyzer.scoreLabel,
|
|
55
66
|
langcode,
|
|
67
|
+
chunk.band,
|
|
56
68
|
$t
|
|
57
69
|
),
|
|
58
70
|
impact: analyzer.impactForScore(chunk.score),
|
|
59
71
|
score: chunk.score,
|
|
60
72
|
targets
|
|
61
|
-
}
|
|
73
|
+
};
|
|
74
|
+
if (chunk.band === "hard") {
|
|
75
|
+
hardNodes.push(node);
|
|
76
|
+
} else {
|
|
77
|
+
okNodes.push(node);
|
|
78
|
+
}
|
|
62
79
|
}
|
|
63
80
|
}
|
|
64
|
-
|
|
81
|
+
const results = [];
|
|
82
|
+
results.push({
|
|
65
83
|
id: "low-readability",
|
|
66
84
|
title: $t("analyzerReadabiliyTitle", "Text readability issues"),
|
|
67
85
|
category: "text",
|
|
@@ -70,10 +88,26 @@ async function analyzeViaProvider(readabilityProvider, langcode, $t) {
|
|
|
70
88
|
"Avoid texts that are hard to read."
|
|
71
89
|
),
|
|
72
90
|
link: "https://en.wikipedia.org/wiki/Readability",
|
|
73
|
-
status:
|
|
74
|
-
nodes,
|
|
75
|
-
impact: summarizeImpact(
|
|
76
|
-
};
|
|
91
|
+
status: hardNodes.length ? "violation" : "pass",
|
|
92
|
+
nodes: hardNodes,
|
|
93
|
+
impact: summarizeImpact(hardNodes)
|
|
94
|
+
});
|
|
95
|
+
if (okNodes.length) {
|
|
96
|
+
results.push({
|
|
97
|
+
id: "ok-readability",
|
|
98
|
+
title: $t("analyzerReadabiliyOkTitle", "Text could be simpler"),
|
|
99
|
+
category: "text",
|
|
100
|
+
description: $t(
|
|
101
|
+
"analyzerReadabiliyOkDescription",
|
|
102
|
+
"Text that could be easier to read."
|
|
103
|
+
),
|
|
104
|
+
link: "https://en.wikipedia.org/wiki/Readability",
|
|
105
|
+
status: "incomplete",
|
|
106
|
+
nodes: okNodes,
|
|
107
|
+
impact: summarizeImpact(okNodes)
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
77
111
|
}
|
|
78
112
|
export default defineAnalyzer(() => {
|
|
79
113
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnalyzerContext } from './helpers/Context.js';
|
|
2
|
+
import type { TextProvider } from '#blokkli/editor/providers/texts';
|
|
2
3
|
export type AnalyzeStatus = 'pass' | 'incomplete' | 'inapplicable' | 'violation';
|
|
3
4
|
export type AnalyzeImpact = 'minor' | 'moderate' | 'serious' | 'critical';
|
|
4
5
|
export type AnalyzeCategory = 'accessibility' | 'seo' | 'text' | 'content';
|
|
@@ -55,8 +56,8 @@ export type Analyzer = {
|
|
|
55
56
|
* - 'generic': Any other type of analysis (accessibility, SEO, custom checks)
|
|
56
57
|
*/
|
|
57
58
|
type?: AnalyzerType;
|
|
58
|
-
label?: string | ((langcode: string) => string);
|
|
59
|
-
description?: string | ((langcode: string) => string);
|
|
59
|
+
label?: string | ((langcode: string, $t: TextProvider) => string);
|
|
60
|
+
description?: string | ((langcode: string, $t: TextProvider) => string);
|
|
60
61
|
/**
|
|
61
62
|
* If true, the raw page (without editor UI) is required for this analyzer.
|
|
62
63
|
*/
|