foldkit 0.120.0 → 0.121.0

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.
@@ -1,4 +1,4 @@
1
- import { Stream } from 'effect';
1
+ import { Option, Stream } from 'effect';
2
2
  /**
3
3
  * Configuration for the `fromEvent` Stream helper.
4
4
  *
@@ -18,6 +18,80 @@ export type FromEventConfig<EventType extends Event, Message> = Readonly<{
18
18
  toMessage: (event: EventType) => Message;
19
19
  options?: AddEventListenerOptions;
20
20
  }>;
21
+ /**
22
+ * Configuration for the `fromEventFilterMap` Stream helper.
23
+ *
24
+ * `target` is read inside the acquire Effect, never before it, so the
25
+ * resolved `EventTarget` is captured at the moment the Subscription's scope
26
+ * opens. Pass a thunk when the target may not exist until the scope opens, or
27
+ * pass the `EventTarget` directly for always-present globals like `window` or
28
+ * `document`.
29
+ *
30
+ * `toMessage(event)` returns `Option.some(message)` to emit a Message for the
31
+ * event, or `Option.none()` to ignore it. The mapper runs synchronously in the
32
+ * same call stack as the browser's event dispatch, so calling
33
+ * `event.preventDefault()` inside it works as expected.
34
+ */
35
+ export type FromEventFilterMapConfig<EventType extends Event, Message> = Readonly<{
36
+ target: EventTarget | (() => EventTarget);
37
+ type: string;
38
+ toMessage: (event: EventType) => Option.Option<Message>;
39
+ options?: AddEventListenerOptions;
40
+ }>;
41
+ /**
42
+ * Build a Stream that emits a Message for the dispatches of a DOM event the
43
+ * mapper chooses to keep, registering the listener when the Stream's scope
44
+ * opens and removing it when the scope closes.
45
+ *
46
+ * This is the filtered variant of `fromEvent`. Its `toMessage` returns
47
+ * `Option.some(message)` to emit and `Option.none()` to ignore the event, so a
48
+ * single listener can react to some dispatches while passing on the rest.
49
+ *
50
+ * Reach for this over a downstream `Stream.filterMap` whenever the decision to
51
+ * keep an event is paired with `event.preventDefault()`. The mapper runs
52
+ * synchronously inside the browser's event dispatch, so `preventDefault()`
53
+ * takes effect, while a downstream filter would run on a later turn after the
54
+ * default action has already happened.
55
+ *
56
+ * The listener lifecycle uses `Effect.acquireRelease`. The `addEventListener`
57
+ * call happens inside the acquire Effect, and the matching
58
+ * `removeEventListener` is registered only after acquire completes, so the
59
+ * listener never leaks on interruption.
60
+ *
61
+ * This is a Stream, not a Subscription entry. Wrap it with
62
+ * `Subscription.persistent` for a listener whose lifetime spans the whole
63
+ * Subscriptions record, or plug it into a `Subscription.make` entry's
64
+ * `dependenciesToStream` (typically behind `Stream.when`) to gate it on a
65
+ * Model condition.
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const subscriptions = Subscription.make<Model, Message>()(entry => ({
70
+ * searchShortcut: entry(
71
+ * { isListening: S.Boolean },
72
+ * {
73
+ * modelToDependencies: model => ({ isListening: model.isListening }),
74
+ * dependenciesToStream: ({ isListening }) =>
75
+ * Stream.when(
76
+ * Subscription.fromEventFilterMap<KeyboardEvent, Message>({
77
+ * target: window,
78
+ * type: 'keydown',
79
+ * toMessage: event => {
80
+ * if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
81
+ * event.preventDefault()
82
+ * return Option.some(OpenedSearch())
83
+ * }
84
+ * return Option.none()
85
+ * },
86
+ * }),
87
+ * Effect.sync(() => isListening),
88
+ * ),
89
+ * },
90
+ * ),
91
+ * }))
92
+ * ```
93
+ */
94
+ export declare const fromEventFilterMap: <EventType extends Event, Message>(config: FromEventFilterMapConfig<EventType, Message>) => Stream.Stream<Message>;
21
95
  /**
22
96
  * Build a Stream that emits a Message for every dispatch of a DOM event,
23
97
  * registering the listener when the Stream's scope opens and removing it when
@@ -34,6 +108,9 @@ export type FromEventConfig<EventType extends Event, Message> = Readonly<{
34
108
  * `dependenciesToStream` (typically behind `Stream.when`) to gate it on a
35
109
  * Model condition.
36
110
  *
111
+ * For a listener that reacts to only some events, reach for
112
+ * `fromEventFilterMap`, whose mapper returns `Option<Message>`.
113
+ *
37
114
  * @example
38
115
  * ```typescript
39
116
  * const subscriptions = Subscription.make<Model, Message>()(entry => ({
@@ -1 +1 @@
1
- {"version":3,"file":"fromEvent.d.ts","sourceRoot":"","sources":["../../src/subscription/fromEvent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE9C;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,CAAC,SAAS,SAAS,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IACvE,MAAM,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,CAAA;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAA;IACxC,OAAO,CAAC,EAAE,uBAAuB,CAAA;CAClC,CAAC,CAAA;AAMF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS,SAAS,KAAK,EAAE,OAAO,EACxD,QAAQ,eAAe,CAAC,SAAS,EAAE,OAAO,CAAC,KAC1C,MAAM,CAAC,MAAM,CAAC,OAAO,CAmBrB,CAAA"}
1
+ {"version":3,"file":"fromEvent.d.ts","sourceRoot":"","sources":["../../src/subscription/fromEvent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,MAAM,EAAS,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,CAAC,SAAS,SAAS,KAAK,EAAE,OAAO,IAAI,QAAQ,CAAC;IACvE,MAAM,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,CAAA;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAA;IACxC,OAAO,CAAC,EAAE,uBAAuB,CAAA;CAClC,CAAC,CAAA;AAMF;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,wBAAwB,CAClC,SAAS,SAAS,KAAK,EACvB,OAAO,IACL,QAAQ,CAAC;IACX,MAAM,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,CAAA;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACvD,OAAO,CAAC,EAAE,uBAAuB,CAAA;CAClC,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,SAAS,KAAK,EAAE,OAAO,EACjE,QAAQ,wBAAwB,CAAC,SAAS,EAAE,OAAO,CAAC,KACnD,MAAM,CAAC,MAAM,CAAC,OAAO,CAsBrB,CAAA;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS,SAAS,KAAK,EAAE,OAAO,EACxD,QAAQ,eAAe,CAAC,SAAS,EAAE,OAAO,CAAC,KAC1C,MAAM,CAAC,MAAM,CAAC,OAAO,CAIpB,CAAA"}
@@ -1,9 +1,19 @@
1
- import { Effect, Queue, Stream } from 'effect';
1
+ import { Effect, Option, Queue, Stream } from 'effect';
2
2
  const resolveTarget = (target) => (typeof target === 'function' ? target() : target);
3
3
  /**
4
- * Build a Stream that emits a Message for every dispatch of a DOM event,
5
- * registering the listener when the Stream's scope opens and removing it when
6
- * the scope closes.
4
+ * Build a Stream that emits a Message for the dispatches of a DOM event the
5
+ * mapper chooses to keep, registering the listener when the Stream's scope
6
+ * opens and removing it when the scope closes.
7
+ *
8
+ * This is the filtered variant of `fromEvent`. Its `toMessage` returns
9
+ * `Option.some(message)` to emit and `Option.none()` to ignore the event, so a
10
+ * single listener can react to some dispatches while passing on the rest.
11
+ *
12
+ * Reach for this over a downstream `Stream.filterMap` whenever the decision to
13
+ * keep an event is paired with `event.preventDefault()`. The mapper runs
14
+ * synchronously inside the browser's event dispatch, so `preventDefault()`
15
+ * takes effect, while a downstream filter would run on a later turn after the
16
+ * default action has already happened.
7
17
  *
8
18
  * The listener lifecycle uses `Effect.acquireRelease`. The `addEventListener`
9
19
  * call happens inside the acquire Effect, and the matching
@@ -19,16 +29,22 @@ const resolveTarget = (target) => (typeof target === 'function' ? target() : tar
19
29
  * @example
20
30
  * ```typescript
21
31
  * const subscriptions = Subscription.make<Model, Message>()(entry => ({
22
- * shortcut: entry(
32
+ * searchShortcut: entry(
23
33
  * { isListening: S.Boolean },
24
34
  * {
25
35
  * modelToDependencies: model => ({ isListening: model.isListening }),
26
36
  * dependenciesToStream: ({ isListening }) =>
27
37
  * Stream.when(
28
- * Subscription.fromEvent<KeyboardEvent, Message>({
38
+ * Subscription.fromEventFilterMap<KeyboardEvent, Message>({
29
39
  * target: window,
30
40
  * type: 'keydown',
31
- * toMessage: event => PressedKey({ key: event.key }),
41
+ * toMessage: event => {
42
+ * if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
43
+ * event.preventDefault()
44
+ * return Option.some(OpenedSearch())
45
+ * }
46
+ * return Option.none()
47
+ * },
32
48
  * }),
33
49
  * Effect.sync(() => isListening),
34
50
  * ),
@@ -37,14 +53,61 @@ const resolveTarget = (target) => (typeof target === 'function' ? target() : tar
37
53
  * }))
38
54
  * ```
39
55
  */
40
- export const fromEvent = (config) => Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
56
+ export const fromEventFilterMap = (config) => Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
41
57
  const target = resolveTarget(config.target);
42
58
  const handleEvent = (event) => {
43
59
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
44
- Queue.offerUnsafe(queue, config.toMessage(event));
60
+ const maybeMessage = config.toMessage(event);
61
+ if (Option.isSome(maybeMessage)) {
62
+ Queue.offerUnsafe(queue, maybeMessage.value);
63
+ }
45
64
  };
46
65
  target.addEventListener(config.type, handleEvent, config.options);
47
66
  return { target, handleEvent };
48
67
  }), ({ target, handleEvent }) => Effect.sync(() => {
49
68
  target.removeEventListener(config.type, handleEvent, config.options);
50
69
  })).pipe(Effect.flatMap(() => Effect.never)));
70
+ /**
71
+ * Build a Stream that emits a Message for every dispatch of a DOM event,
72
+ * registering the listener when the Stream's scope opens and removing it when
73
+ * the scope closes.
74
+ *
75
+ * The listener lifecycle uses `Effect.acquireRelease`. The `addEventListener`
76
+ * call happens inside the acquire Effect, and the matching
77
+ * `removeEventListener` is registered only after acquire completes, so the
78
+ * listener never leaks on interruption.
79
+ *
80
+ * This is a Stream, not a Subscription entry. Wrap it with
81
+ * `Subscription.persistent` for a listener whose lifetime spans the whole
82
+ * Subscriptions record, or plug it into a `Subscription.make` entry's
83
+ * `dependenciesToStream` (typically behind `Stream.when`) to gate it on a
84
+ * Model condition.
85
+ *
86
+ * For a listener that reacts to only some events, reach for
87
+ * `fromEventFilterMap`, whose mapper returns `Option<Message>`.
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * const subscriptions = Subscription.make<Model, Message>()(entry => ({
92
+ * shortcut: entry(
93
+ * { isListening: S.Boolean },
94
+ * {
95
+ * modelToDependencies: model => ({ isListening: model.isListening }),
96
+ * dependenciesToStream: ({ isListening }) =>
97
+ * Stream.when(
98
+ * Subscription.fromEvent<KeyboardEvent, Message>({
99
+ * target: window,
100
+ * type: 'keydown',
101
+ * toMessage: event => PressedKey({ key: event.key }),
102
+ * }),
103
+ * Effect.sync(() => isListening),
104
+ * ),
105
+ * },
106
+ * ),
107
+ * }))
108
+ * ```
109
+ */
110
+ export const fromEvent = (config) => fromEventFilterMap({
111
+ ...config,
112
+ toMessage: event => Option.some(config.toMessage(event)),
113
+ });
@@ -2,6 +2,6 @@ export { aggregate, lift, make, persistent } from '../runtime/subscription.js';
2
2
  export type { EntryWithoutKeepAlive, Subscription, Subscriptions, } from '../runtime/subscription.js';
3
3
  export { animationFrame } from './animationFrame.js';
4
4
  export type { AnimationFrameConfig } from './animationFrame.js';
5
- export { fromEvent } from './fromEvent.js';
6
- export type { FromEventConfig } from './fromEvent.js';
5
+ export { fromEvent, fromEventFilterMap } from './fromEvent.js';
6
+ export type { FromEventConfig, FromEventFilterMapConfig } from './fromEvent.js';
7
7
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/subscription/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAE9E,YAAY,EACV,qBAAqB,EACrB,YAAY,EACZ,aAAa,GACd,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAE/D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/subscription/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAE9E,YAAY,EACV,qBAAqB,EACrB,YAAY,EACZ,aAAa,GACd,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAE/D,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAE9D,YAAY,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA"}
@@ -1,3 +1,3 @@
1
1
  export { aggregate, lift, make, persistent } from '../runtime/subscription.js';
2
2
  export { animationFrame } from './animationFrame.js';
3
- export { fromEvent } from './fromEvent.js';
3
+ export { fromEvent, fromEventFilterMap } from './fromEvent.js';
@@ -1,11 +1,12 @@
1
1
  import { Schema as S } from 'effect';
2
- import type { Html } from '../../html/index.js';
3
- export declare const Model: S.Struct<{}>;
4
- export type Model = typeof Model.Type;
2
+ import type { Attribute, Html } from '../../html/index.js';
5
3
  export declare const IgnoredInteraction: import("../../schema/index.js").CallableTaggedStruct<"IgnoredInteraction", {}>;
6
4
  export declare const Message: S.Union<readonly [import("../../schema/index.js").CallableTaggedStruct<"IgnoredInteraction", {}>]>;
7
5
  export type Message = typeof Message.Type;
8
- export declare const initialModel: Model;
6
+ export type Model = Readonly<{
7
+ attribute: Attribute<Message>;
8
+ }>;
9
9
  export declare const update: (model: Model, message: Message) => readonly [Model, ReadonlyArray<never>];
10
- export declare const view: (_model: Model) => Html;
11
- //# sourceMappingURL=svgAttributes.d.ts.map
10
+ export declare const testId = "attribute-host";
11
+ export declare const view: (model: Model) => Html;
12
+ //# sourceMappingURL=attributes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/test/apps/attributes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhD,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAA;AAM1D,eAAO,MAAM,kBAAkB,gFAA0B,CAAA;AAEzD,eAAO,MAAM,OAAO,oGAAgC,CAAA;AACpD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,MAAM,MAAM,KAAK,GAAG,QAAQ,CAAC;IAAE,SAAS,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAAA;AAI/D,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,CAMrC,CAAA;AAMH,eAAO,MAAM,MAAM,mBAAU,CAAA;AAE7B,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAInC,CAAA"}
@@ -0,0 +1,17 @@
1
+ import { Match as M, Schema as S } from 'effect';
2
+ import { html } from '../../html/index.js';
3
+ import { m } from '../../message/index.js';
4
+ // MESSAGE
5
+ export const IgnoredInteraction = m('IgnoredInteraction');
6
+ export const Message = S.Union([IgnoredInteraction]);
7
+ // UPDATE
8
+ export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
9
+ IgnoredInteraction: () => [model, []],
10
+ }));
11
+ // VIEW
12
+ const TEST_ID = 'attribute-host';
13
+ export const testId = TEST_ID;
14
+ export const view = (model) => {
15
+ const h = html();
16
+ return h.div([h.DataAttribute('testid', TEST_ID), model.attribute], []);
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit",
3
- "version": "0.120.0",
3
+ "version": "0.121.0",
4
4
  "description": "A TypeScript frontend framework, built on Effect and architected like Elm",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1 +0,0 @@
1
- {"version":3,"file":"svgAttributes.d.ts","sourceRoot":"","sources":["../../../src/test/apps/svgAttributes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAA;AAM/C,eAAO,MAAM,KAAK,cAAe,CAAA;AACjC,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,eAAO,MAAM,kBAAkB,gFAA0B,CAAA;AAEzD,eAAO,MAAM,OAAO,oGAAgC,CAAA;AACpD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,eAAO,MAAM,YAAY,EAAE,KAAU,CAAA;AAIrC,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,CAMrC,CAAA;AAIH,eAAO,MAAM,IAAI,GAAI,QAAQ,KAAK,KAAG,IAgHpC,CAAA"}
@@ -1,106 +0,0 @@
1
- import { Match as M, Schema as S } from 'effect';
2
- import { html } from '../../html/index.js';
3
- import { m } from '../../message/index.js';
4
- // MODEL
5
- export const Model = S.Struct({});
6
- // MESSAGE
7
- export const IgnoredInteraction = m('IgnoredInteraction');
8
- export const Message = S.Union([IgnoredInteraction]);
9
- // INIT
10
- export const initialModel = {};
11
- // UPDATE
12
- export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
13
- IgnoredInteraction: () => [model, []],
14
- }));
15
- // VIEW
16
- export const view = (_model) => {
17
- const h = html();
18
- return h.svg([
19
- h.DataAttribute('testid', 'svg-root'),
20
- h.ViewBox('0 0 100 100'),
21
- h.PreserveAspectRatio('xMidYMid meet'),
22
- h.Color('black'),
23
- h.Overflow('visible'),
24
- ], [
25
- h.text([
26
- h.DataAttribute('testid', 'svg-text'),
27
- h.Dx('1'),
28
- h.Dy('2'),
29
- h.Rotate('15'),
30
- h.TextAnchor('middle'),
31
- h.DominantBaseline('central'),
32
- h.AlignmentBaseline('middle'),
33
- h.BaselineShift('super'),
34
- h.TextLength('40'),
35
- h.LengthAdjust('spacing'),
36
- h.FontFamily('serif'),
37
- h.FontSize('12'),
38
- h.FontWeight('bold'),
39
- h.FontStyle('italic'),
40
- h.LetterSpacing('1'),
41
- h.WordSpacing('2'),
42
- h.TextDecoration('underline'),
43
- h.WritingMode('horizontal-tb'),
44
- h.Visibility('visible'),
45
- h.Display('inline'),
46
- h.Cursor('pointer'),
47
- h.PointerEvents('none'),
48
- ], ['Label']),
49
- h.rect([
50
- h.DataAttribute('testid', 'svg-rect'),
51
- h.Rx('4'),
52
- h.Ry('4'),
53
- h.PathLength('100'),
54
- h.FillOpacity('0.5'),
55
- h.StrokeOpacity('0.8'),
56
- h.StrokeMiterlimit('2'),
57
- h.PaintOrder('stroke'),
58
- h.VectorEffect('non-scaling-stroke'),
59
- h.ShapeRendering('crispEdges'),
60
- h.TextRendering('optimizeLegibility'),
61
- h.ImageRendering('pixelated'),
62
- h.ClipPath('url(#clip)'),
63
- h.Mask('url(#mask)'),
64
- h.Filter('url(#filter)'),
65
- h.MarkerStart('url(#start)'),
66
- h.MarkerMid('url(#mid)'),
67
- h.MarkerEnd('url(#end)'),
68
- ], []),
69
- h.linearGradient([
70
- h.DataAttribute('testid', 'svg-gradient'),
71
- h.GradientUnits('userSpaceOnUse'),
72
- h.GradientTransform('rotate(45)'),
73
- h.SpreadMethod('reflect'),
74
- h.Fx('0.1'),
75
- h.Fy('0.2'),
76
- h.Fr('0.3'),
77
- h.ClipPathUnits('userSpaceOnUse'),
78
- h.MaskUnits('userSpaceOnUse'),
79
- h.MaskContentUnits('userSpaceOnUse'),
80
- h.FilterUnits('userSpaceOnUse'),
81
- h.PrimitiveUnits('userSpaceOnUse'),
82
- ], [
83
- h.stop([
84
- h.DataAttribute('testid', 'svg-stop'),
85
- h.Offset('0.5'),
86
- h.StopColor('red'),
87
- h.StopOpacity('0.9'),
88
- ], []),
89
- ]),
90
- h.pattern([
91
- h.DataAttribute('testid', 'svg-pattern'),
92
- h.PatternUnits('userSpaceOnUse'),
93
- h.PatternContentUnits('userSpaceOnUse'),
94
- h.PatternTransform('scale(2)'),
95
- ], []),
96
- h.marker([
97
- h.DataAttribute('testid', 'svg-marker'),
98
- h.MarkerWidth('6'),
99
- h.MarkerHeight('6'),
100
- h.MarkerUnits('strokeWidth'),
101
- h.RefX('3'),
102
- h.RefY('3'),
103
- h.Orient('auto'),
104
- ], []),
105
- ]);
106
- };