diff --git a/lib/filters/add_class/definition.js b/lib/filters/add_class/definition.js
new file mode 100644
index 0000000..1977317
--- /dev/null
+++ b/lib/filters/add_class/definition.js
@@ -0,0 +1,40 @@
+/**
+ * @file The safe_join filter
+ *
+ * Docs for TwigExtension::safeJoin (Drupal 9.3.x):
+ *
+ * ```
+ * new TwigFilter('safe_join',
+ * [$this, 'safeJoin'],
+ * [
+ * 'needs_environment' => TRUE,
+ * 'is_safe' => ['html']
+ * ]
+ * )
+ * ```
+ *
+ * ```
+ * Joins several strings together safely.
+ *
+ * @param \Twig\Environment $env
+ * A Twig Environment instance.
+ * @param mixed[]|\Traversable|null $value
+ * The pieces to join.
+ * @param string $glue
+ * The delimiter with which to join the string. Defaults to an empty string.
+ * This value is expected to be safe for output and user provided data
+ * should never be used as a glue.
+ *
+ * @return string
+ * The strings joined together.
+ * ```
+ */
+
+export const name = 'add_class';
+
+export const options = {
+ needs_environment: true,
+ is_safe: ['html'],
+};
+
+export const acceptedArguments = [{ name: 'className', defaultValue: '' }];
diff --git a/lib/filters/add_class/twing.js b/lib/filters/add_class/twing.js
new file mode 100644
index 0000000..dda66ea
--- /dev/null
+++ b/lib/filters/add_class/twing.js
@@ -0,0 +1,21 @@
+import { newTwingFilter } from '../../helpers/twing.js';
+import { name, options, acceptedArguments } from './definition.js';
+
+function surround(value, className) {
+ if (Array.isArray(className)) {
+ className = className.join(' ');
+ }
+ return `
${value}
`;
+}
+export function callable(value, clasName) {
+ if (typeof value === 'object') {
+ const output = [];
+ Object.values(value).forEach((value) => {
+ output.push(surround(value, clasName));
+ });
+ return output.join(' ');
+ }
+ return surround(value, clasName);
+}
+
+export default newTwingFilter(name, callable, options, acceptedArguments);
diff --git a/lib/filters/set_attribute/definition.js b/lib/filters/set_attribute/definition.js
new file mode 100644
index 0000000..537111e
--- /dev/null
+++ b/lib/filters/set_attribute/definition.js
@@ -0,0 +1,11 @@
+export const name = 'set_attribute';
+
+export const options = {
+ needs_environment: true,
+ is_safe: ['html'],
+};
+
+export const acceptedArguments = [
+ { name: 'attributeName', defaultValue: '' },
+ { name: 'attributeValue', defaultValue: '' },
+];
diff --git a/lib/filters/set_attribute/twing.js b/lib/filters/set_attribute/twing.js
new file mode 100644
index 0000000..68365ae
--- /dev/null
+++ b/lib/filters/set_attribute/twing.js
@@ -0,0 +1,18 @@
+import { newTwingFilter } from '../../helpers/twing.js';
+import { name, options, acceptedArguments } from './definition.js';
+
+function surround(value, attributeName, attributeValue) {
+ return `${value}
`;
+}
+export function callable(value, attributeName, attributeValue) {
+ if (typeof value === 'object') {
+ const output = [];
+ Object.values(value).forEach((value) => {
+ output.push(surround(value, attributeName, attributeValue));
+ });
+ return output.join(' ');
+ }
+ return surround(value, attributeName, attributeValue);
+}
+
+export default newTwingFilter(name, callable, options, acceptedArguments);
diff --git a/lib/filters/twing.js b/lib/filters/twing.js
index 40eb942..10ea473 100644
--- a/lib/filters/twing.js
+++ b/lib/filters/twing.js
@@ -4,6 +4,8 @@ import cleanIdFilter from './clean_id/twing.js';
import drupalEscapeFilter from './drupal_escape/twing.js';
import formatDateFilter from './format_date/twing.js';
import placeholderFilter from './placeholder/twing.js';
+import addClassFilter from './add_class/twing.js';
+import setAttributeFilter from './set_attribute/twing.js';
import renderFilter from './render/twing.js';
import safeJoinFilter from './safe_join/twing.js';
import {
@@ -18,7 +20,9 @@ import withoutFilter from './without/twing.js';
const filters = [
cleanClassFilter,
+ setAttributeFilter,
cleanIdFilter,
+ addClassFilter,
drupalEscapeFilter,
formatDateFilter,
placeholderFilter,
diff --git a/tests/Twing/filters/add_class.js b/tests/Twing/filters/add_class.js
new file mode 100644
index 0000000..adf2bf2
--- /dev/null
+++ b/tests/Twing/filters/add_class.js
@@ -0,0 +1,31 @@
+import test from 'ava';
+import { setupTwingBefore, renderTemplateMacro } from '#twing-fixture';
+
+test.before(setupTwingBefore);
+
+const template = `{{ content|add_class("sample-class") }}`;
+
+test('Add class to simple content', renderTemplateMacro, {
+ template,
+ data: {
+ content: 'Inner content',
+ },
+ expected: 'Inner content
',
+});
+
+test('Add class to content array', renderTemplateMacro, {
+ template,
+ data: {
+ content: ['Inner content', 'Inner content2'],
+ },
+ expected:
+ 'Inner content
Inner content2
',
+});
+
+test('Add array class to simple content', renderTemplateMacro, {
+ template: `{{ content|add_class(["sample-class" , "sample-class2"]) }}`,
+ data: {
+ content: 'Inner content',
+ },
+ expected: 'Inner content
',
+});
diff --git a/tests/Twing/filters/set_attribute.js b/tests/Twing/filters/set_attribute.js
new file mode 100644
index 0000000..7f6bd18
--- /dev/null
+++ b/tests/Twing/filters/set_attribute.js
@@ -0,0 +1,14 @@
+import test from 'ava';
+import { setupTwingBefore, renderTemplateMacro } from '#twing-fixture';
+
+test.before(setupTwingBefore);
+
+const template = `{{ content|set_attribute('role', "xxx") }}`;
+
+test('set attribute to simple content', renderTemplateMacro, {
+ template,
+ data: {
+ content: 'Inner content',
+ },
+ expected: 'Inner content
',
+});