@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.
Files changed (61) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +3 -4
  3. package/dist/modules/agent/runtime/app/composables/agentProvider.js +28 -19
  4. package/dist/modules/agent/runtime/app/tools/check_readability/index.js +1 -0
  5. package/dist/modules/agent/runtime/app/tools/delegate_text_rewrite/Component.d.vue.ts +2 -0
  6. package/dist/modules/agent/runtime/app/tools/delegate_text_rewrite/Component.vue +7 -3
  7. package/dist/modules/agent/runtime/app/tools/delegate_text_rewrite/Component.vue.d.ts +2 -0
  8. package/dist/modules/agent/runtime/app/tools/helpers.js +5 -3
  9. package/dist/modules/agent/runtime/server/Session.d.ts +2 -0
  10. package/dist/modules/agent/runtime/server/Session.js +17 -0
  11. package/dist/modules/agent/runtime/server/agent.js +2 -1
  12. package/dist/modules/agent/runtime/shared/types.d.ts +1 -0
  13. package/dist/modules/agent/runtime/shared/types.js +2 -1
  14. package/dist/runtime/components/BlokkliProvider.vue +14 -13
  15. package/dist/runtime/editor/components/Toolbar/index.vue +1 -2
  16. package/dist/runtime/editor/composables/useStickyToolbar.d.ts +1 -1
  17. package/dist/runtime/editor/composables/useStickyToolbar.js +25 -7
  18. package/dist/runtime/editor/css/output.css +1 -1
  19. package/dist/runtime/editor/features/analyze/Main.d.vue.ts +6 -0
  20. package/dist/runtime/editor/features/analyze/Main.vue +26 -1
  21. package/dist/runtime/editor/features/analyze/Main.vue.d.ts +6 -0
  22. package/dist/runtime/editor/features/analyze/Renderer/index.d.vue.ts +2 -0
  23. package/dist/runtime/editor/features/analyze/Renderer/index.vue +86 -15
  24. package/dist/runtime/editor/features/analyze/Renderer/index.vue.d.ts +2 -0
  25. package/dist/runtime/editor/features/analyze/analyzers/altText.d.ts +2 -0
  26. package/dist/runtime/editor/features/analyze/analyzers/altText.js +60 -0
  27. package/dist/runtime/editor/features/analyze/analyzers/headingStructure.d.ts +2 -0
  28. package/dist/runtime/editor/features/analyze/analyzers/headingStructure.js +141 -0
  29. package/dist/runtime/editor/features/analyze/analyzers/index.d.ts +5 -1
  30. package/dist/runtime/editor/features/analyze/analyzers/index.js +11 -1
  31. package/dist/runtime/editor/features/analyze/analyzers/readability.js +50 -16
  32. package/dist/runtime/editor/features/analyze/analyzers/types.d.ts +3 -2
  33. package/dist/runtime/editor/features/analyze/index.vue +12 -0
  34. package/dist/runtime/editor/features/analyze/readability/builtinAnalyzer.js +38 -22
  35. package/dist/runtime/editor/features/analyze/readability/types.d.ts +18 -3
  36. package/dist/runtime/editor/features/dragging-overlay/DragItems/index.d.vue.ts +1 -0
  37. package/dist/runtime/editor/features/dragging-overlay/DragItems/index.vue +102 -2
  38. package/dist/runtime/editor/features/dragging-overlay/DragItems/index.vue.d.ts +1 -0
  39. package/dist/runtime/editor/features/dragging-overlay/Renderer/index.d.vue.ts +1 -0
  40. package/dist/runtime/editor/features/dragging-overlay/Renderer/index.vue +16 -1
  41. package/dist/runtime/editor/features/dragging-overlay/Renderer/index.vue.d.ts +1 -0
  42. package/dist/runtime/editor/features/dragging-overlay/index.vue +2 -1
  43. package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/ChunkOverlay.d.vue.ts +8 -0
  44. package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/ChunkOverlay.vue +135 -0
  45. package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/ChunkOverlay.vue.d.ts +8 -0
  46. package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/index.d.vue.ts +7 -0
  47. package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/index.vue +187 -0
  48. package/dist/runtime/editor/features/editable-field/Overlay/ReadabilityIndicator/index.vue.d.ts +7 -0
  49. package/dist/runtime/editor/features/editable-field/Overlay/index.vue +23 -0
  50. package/dist/runtime/editor/features/options/index.vue +4 -3
  51. package/dist/runtime/editor/features/search/Overlay/Results/Content/index.vue +7 -11
  52. package/dist/runtime/editor/features/search/Overlay/Results/Page/index.vue +11 -13
  53. package/dist/runtime/editor/features/selection/Renderer/index.vue +2 -0
  54. package/dist/runtime/editor/features/translations/index.vue +1 -1
  55. package/dist/runtime/editor/providers/analyze.js +1 -1
  56. package/dist/runtime/editor/providers/readability.js +16 -20
  57. package/dist/runtime/editor/translations/de.json +113 -1
  58. package/dist/runtime/editor/translations/fr.json +113 -1
  59. package/dist/runtime/editor/translations/gsw_CH.json +113 -1
  60. package/dist/runtime/editor/translations/it.json +113 -1
  61. 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
- <div />
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("mouse:up", (e) => {
231
- const artboardX = (e.x - ui.artboardOffset.value.x) / ui.artboardScale.value;
232
- const artboardY = (e.y - ui.artboardOffset.value.y) / ui.artboardScale.value;
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
- const id = node.id + "_____" + node.index;
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,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -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,2 @@
1
+ declare const _default: (options?: object | undefined) => import("./types.js").Analyzer;
2
+ export default _default;
@@ -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
- export { defineAnalyzer };
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
- export { defineAnalyzer };
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
- parts.push(
19
- $t("analyzerReadabiliyHardToRead", `Hard to read (@lang).`).replace(
20
- "@lang",
21
- lang.toUpperCase()
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 nodes = [];
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 !== "hard") continue;
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
- nodes.push({
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
- return {
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: nodes.length ? "violation" : "pass",
74
- nodes,
75
- impact: summarizeImpact(nodes)
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
  */