diff --git a/.editorconfig b/.editorconfig index 8428577b..10bc0533 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,7 @@ root = true # Top-most EditorConfig file [*] end_of_line = lf -[*.{js,jsx,ts,tsx,vue,sh}] +[*.{js,jsx,ts,tsx,vue,sh,scss}] indent_style = space indent_size = 2 trim_trailing_whitespace = true @@ -24,3 +24,10 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true + +[*.{scss}] # SASS guidelines: https://archive.today/2024.02.16-232553/https://sass-guidelin.es/ +indent_style = space +indent_size = 2 # Recommended by SASS guidelines +max_line_length = 100 # Recommended by SASS guidelines +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/2 b/2 deleted file mode 100644 index 7d127335..00000000 --- a/2 +++ /dev/null @@ -1,31 +0,0 @@ -Show error on AV removal on desktop $264, $304 - -This solves $264 where users do not get error messages when running -script file fails due to antivirus intervention (it being blocking the -script file as soon as privacy.sexy generates it to run it). Now if the -desktop app users tries to save or run a script file and it afils due to -antivirus removal, they'll get a special error message with guiding next -steps. - -- Add additional check to able to fail if the file writing fails. This - includes trying to reading the written file back as suggested in $304. - This successfully detects antivirus (Defender) intervation as read - file operation triggers the antivirus scan that deletes the file. -- Show directory and file path in error messages as suggested in $304. -- Show an error message with more detailed information if an antivirus - is detected. - -# Please enter the commit message for your changes. Lines starting -# with '#' will be ignored, and an empty message aborts the commit. -# -# Date: Tue Jan 16 16:23:08 2024 +0100 -# -# On branch master -# Your branch is ahead of 'origin/master' by 1 commit. -# (use "git push" to publish your local commits) -# -# Changes to be committed: -# modified: ../../application/CodeRunner/CodeRunner.ts -# new file: NodeReliableFileWriter.ts -# new file: ReliableFileWriter.ts -# diff --git a/src/presentation/assets/styles/_fonts.scss b/src/presentation/assets/styles/_fonts.scss index e570ba71..ab409ac2 100644 --- a/src/presentation/assets/styles/_fonts.scss +++ b/src/presentation/assets/styles/_fonts.scss @@ -6,7 +6,7 @@ /* slabo-27px-regular - latin_latin-ext */ @font-face { font-display: swap; - + font-family: 'Slabo 27px'; font-style: normal; font-weight: 400; @@ -24,16 +24,6 @@ url('#{$base-assets-path}/fonts/yesteryear-v18-latin-regular.ttf') format('truetype'); /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */ } -/* roboto-slab-regular - latin */ -@font-face { - font-display: swap; - font-family: 'Roboto Slab'; - font-style: normal; - font-weight: 400; - src: url('#{$base-assets-path}/fonts/roboto-slab-v34-latin-regular.woff2') format('woff2'), /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ - url('#{$base-assets-path}/fonts/roboto-slab-v34-latin-regular.ttf') format('truetype'); /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */ -} - /* roboto-slab-regular - cyrillic_cyrillic-ext_greek_greek-ext_latin_latin-ext_vietnamese */ @font-face { font-display: swap; @@ -62,13 +52,3 @@ src: url('#{$base-assets-path}/fonts/source-code-pro-v23-latin-regular.woff2') format('woff2'), /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ url('#{$base-assets-path}/fonts/source-code-pro-v23-latin-regular.ttf') format('truetype'); /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */ } - -/* roboto-mono-regular - latin */ -@font-face { - font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */ - font-family: 'Roboto Mono'; - font-style: normal; - font-weight: 400; - src: url('#{$base-assets-path}/fonts/roboto-mono-v23-latin-regular.woff2') format('woff2'), /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ - url('#{$base-assets-path}/fonts/roboto-mono-v23-latin-regular.ttf') format('truetype'); /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */ -} diff --git a/src/presentation/assets/styles/_globals.scss b/src/presentation/assets/styles/_globals.scss deleted file mode 100644 index 9f0787c3..00000000 --- a/src/presentation/assets/styles/_globals.scss +++ /dev/null @@ -1,29 +0,0 @@ -/* - Defines global styles that applies to globally defined tags by default (body, main, article, div etc.) -*/ - -@use "@/presentation/assets/styles/colors" as *; -@use "@/presentation/assets/styles/mixins" as *; -@use "@/presentation/assets/styles/vite-path" as *; -@use "@/presentation/assets/styles/typography" as *; - -* { - box-sizing: border-box; -} - -a { - color: inherit; - cursor: pointer; - @include flat-button($disabled: false); -} - -body { - background: $color-background; - font-family: $font-family-main; - font-size: $font-size-absolute-normal; -} - -input { - font-family: unset; // Reset browser default -} - diff --git a/src/presentation/assets/styles/_mixins.scss b/src/presentation/assets/styles/_mixins.scss index a65c7983..4a998421 100644 --- a/src/presentation/assets/styles/_mixins.scss +++ b/src/presentation/assets/styles/_mixins.scss @@ -118,3 +118,13 @@ } } } + +@mixin set-property-ch-value-with-fallback($property, $value-in-ch) { + @supports (width: 1ch) { + #{$property}: #{$value-in-ch}ch; + } + // For browsers that does not support `ch` unit (e.g., Opera Mini): + $estimated-width-per-character-in-em: calc(1em / 2); // 1 character is approximately half the font size + $calculated-width-in-em: calc(#{$estimated-width-per-character-in-em} * #{$value-in-ch}); + #{$property}: $calculated-width-in-em; +} diff --git a/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/_code-styling.scss b/src/presentation/assets/styles/base/_code-styling.scss similarity index 80% rename from src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/_code-styling.scss rename to src/presentation/assets/styles/base/_code-styling.scss index c5397143..dd341977 100644 --- a/src/presentation/components/Scripts/View/Tree/NodeContent/Markdown/_code-styling.scss +++ b/src/presentation/assets/styles/base/_code-styling.scss @@ -1,4 +1,7 @@ -@use "@/presentation/assets/styles/main" as *; +@use "@/presentation/assets/styles/colors" as *; +@use "@/presentation/assets/styles/mixins" as *; +@use "@/presentation/assets/styles/vite-path" as *; +@use "@/presentation/assets/styles/typography" as *; @use 'sass:math'; @mixin code-block() { @@ -23,13 +26,14 @@ $color-background, $code-block-padding, ) { - $font-size-code: $font-size-relative-smaller; // Keep relative size to scale right with different text sizes around. + $font-size-code: $font-size-relative-smaller; // Keep relative size to scale right with different text sizes around. $border-radius: 2px; // Subtle rounding still maintaining sharp design. @include base-code { font-family: $font-family-monospace; border-radius: $border-radius; font-size: $font-size-code; + color: $color-on-primary; } @include inline-code { diff --git a/src/presentation/assets/styles/base/_index.scss b/src/presentation/assets/styles/base/_index.scss new file mode 100644 index 00000000..c5a26022 --- /dev/null +++ b/src/presentation/assets/styles/base/_index.scss @@ -0,0 +1,53 @@ +/* + Defines global styles that applies to globally defined tags by default (body, main, article, div etc.). + Styles Fundamental HTML elements. + Contains foundational CSS rules that have a broad impact on the project's styling. + CSS Base applies a style foundation for HTML elements that is consistent for baseline browsers +*/ + +@use "@/presentation/assets/styles/colors" as *; +@use "@/presentation/assets/styles/mixins" as *; +@use "@/presentation/assets/styles/vite-path" as *; +@use "@/presentation/assets/styles/typography" as *; +@use "_code-styling" as *; +@use "_margin-padding" as *; +@use "_link-styling" as *; + +$base-spacing: 1em; + +* { + box-sizing: border-box; +} + +body { + background: $color-background; + font-family: $font-family-main; + font-size: $font-size-absolute-normal; + + @include apply-uniform-spacing($base-spacing: $base-spacing) +} + +input { + font-family: unset; // Reset browser default +} + +blockquote { + padding: 0 $base-spacing; + border-left: .25em solid $color-primary; +} + +@include style-code-elements( + $code-block-padding: $base-spacing, + $color-background: $color-primary-darker, +); + +hr { + opacity: 0.6; +} + +sup { + @include reset-sup; + + vertical-align: super; + font-size: $font-size-relative-smallest; +} diff --git a/src/presentation/assets/styles/base/_link-styling.scss b/src/presentation/assets/styles/base/_link-styling.scss new file mode 100644 index 00000000..d8e037e8 --- /dev/null +++ b/src/presentation/assets/styles/base/_link-styling.scss @@ -0,0 +1,42 @@ +@use "@/presentation/assets/styles/mixins" as *; +@use "@/presentation/assets/styles/typography" as *; +@use 'sass:math'; + +a { + color: inherit; + cursor: pointer; + @include flat-button($disabled: false); + + &[href] { + word-break: break-word; // Enables long URLs to wrap within the container, preventing horizontal overflow. + } + &[href^="http"]{ + &:after { + display: inline-block; + content: ''; + + /* + Use mask element instead of content/background-image etc. + This way we can apply current font color to it to match the theme + */ + mask: url(@/presentation/assets/icons/external-link.svg) no-repeat 50% 50%; + mask-size: cover; + background-color: currentColor; + + /* + Use absolute sizing instead of relative. Relative sizing looks bad and inconsistent if there are external elements + inside small text (such as inside ``) and bigger elements like in bigger text. Making them always have same size + make the text read and flow better. + */ + width: $font-size-absolute-x-small; + height: $font-size-absolute-x-small; + + vertical-align: text-top; + + @include set-property-ch-value-with-fallback( + $property: margin-left, + $value-in-ch: 0.25, + ) + } + } +} diff --git a/src/presentation/assets/styles/base/_margin-padding.scss b/src/presentation/assets/styles/base/_margin-padding.scss new file mode 100644 index 00000000..5fbf4eda --- /dev/null +++ b/src/presentation/assets/styles/base/_margin-padding.scss @@ -0,0 +1,64 @@ +@use 'sass:math'; + +@mixin no-margin($selectors) { + #{$selectors} { + margin: 0; + } +} + +@mixin no-padding($selectors) { + #{$selectors} { + padding: 0; + } +} + +@mixin left-padding($selectors, $horizontal-spacing) { + #{$selectors} { + padding-inline-start: $horizontal-spacing; + } +} + +@mixin bottom-margin($selectors, $vertical-spacing) { + #{$selectors} { + &:not(:last-child) { + margin-bottom: $vertical-spacing; + } + } +} + +@mixin apply-uniform-vertical-spacing($base-vertical-spacing) { + /* Reset default top/bottom margins added by browser. */ + @include no-margin('p'); + @include no-margin('h1, h2, h3, h4, h5, h6'); + @include no-margin('blockquote'); + @include no-margin('pre'); + @include no-margin('hr'); + @include no-margin('ul, ol'); + + /* Add spacing between elements using `margin-bottom` only (bottom-up instead of top-down strategy). */ + $small-vertical-spacing: math.div($base-vertical-spacing, 2); + @include bottom-margin('p', $base-vertical-spacing); + @include bottom-margin('li > p', $small-vertical-spacing); // Reduce margin for paragraphs directly within list items to visually group related content. + @include bottom-margin('h1, h2, h3, h4, h5, h6', $small-vertical-spacing); + @include bottom-margin('ul, ol', $base-vertical-spacing); + @include bottom-margin('li', $small-vertical-spacing); + @include bottom-margin('table', $base-vertical-spacing); + @include bottom-margin('blockquote', $base-vertical-spacing); + @include bottom-margin('pre', $base-vertical-spacing); + @include bottom-margin('article', $base-vertical-spacing); + @include bottom-margin('hr', $base-vertical-spacing); +} + +@mixin apply-uniform-horizontal-spacing($base-horizontal-spacing) { + /* Reset default left/right paddings added by browser. */ + @include no-padding('ul, ol'); + + /* Add spacing for list items. */ + $large-horizontal-spacing: $base-horizontal-spacing * 2; + @include left-padding('ul, ol', $large-horizontal-spacing); +} + +@mixin apply-uniform-spacing($base-spacing) { + @include apply-uniform-vertical-spacing($base-spacing); + @include apply-uniform-horizontal-spacing($base-spacing); +} diff --git a/src/presentation/assets/styles/main.scss b/src/presentation/assets/styles/main.scss index 88d7f7b7..8f5c9dfa 100644 --- a/src/presentation/assets/styles/main.scss +++ b/src/presentation/assets/styles/main.scss @@ -4,7 +4,7 @@ @forward "./typography"; @forward "./media"; @forward "./colors"; -@forward "./globals"; +@forward "./base"; @forward "./mixins"; @forward "./components/card"; diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/InfoTooltip.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/InfoTooltip.vue index 4c35a5fb..885df86c 100644 --- a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/InfoTooltip.vue +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/InfoTooltip.vue @@ -1,12 +1,12 @@ diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/RunInstructions.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/RunInstructions.vue index 7f4c5db3..35b66f03 100644 --- a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/RunInstructions.vue +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/RunInstructions.vue @@ -1,32 +1,37 @@ diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue index bd0ec2f8..4d6efee6 100644 --- a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue @@ -1,16 +1,16 @@ diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/LinuxInstructions.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/LinuxInstructions.vue index a8f88eea..57a870c0 100644 --- a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/LinuxInstructions.vue +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/LinuxInstructions.vue @@ -46,9 +46,7 @@ Navigate to the folder where you downloaded the file e.g.:

- - cd ~/Downloads - + cd ~/Downloads

Press on enter/return key after running the command. @@ -72,9 +70,7 @@ Give the file execute permissions:

- - chmod +x {{ filename }} - + chmod +x {{ filename }}

Press on enter/return key after running the command. @@ -101,11 +97,11 @@ Execute the file:

- - ./{{ filename }} - + ./{{ filename }} - If you have desktop environment, instead of running this command you can alternatively: +

+ If you have desktop environment, instead of running this command you can alternatively: +

  1. Locate the file using your file manager.
  2. Right click on the file, select "Run as program".
  3. diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/MacOsInstructions.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/MacOsInstructions.vue index 9e4fcebb..af3304b6 100644 --- a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/MacOsInstructions.vue +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/MacOsInstructions.vue @@ -49,9 +49,7 @@ Give the file execute permissions:

    - - chmod +x {{ filename }} - + chmod +x {{ filename }}

    Press on enter/return key after running the command. @@ -67,9 +65,7 @@ Execute the file:

    - - ./{{ filename }} - + ./{{ filename }} Alternatively you can locate the file in Finder and double click on it. diff --git a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/WindowsInstructions.vue b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/WindowsInstructions.vue index 9d7f6d6a..8d40828c 100644 --- a/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/WindowsInstructions.vue +++ b/src/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/WindowsInstructions.vue @@ -23,12 +23,12 @@

    In Edge: -

      -
    1. Select Keep from the downloads section.
    2. -
    3. Click Show more on the next warning.
    4. -
    5. Select Keep anyway.
    6. -

    +
      +
    1. Select Keep from the downloads section.
    2. +
    3. Click Show more on the next warning.
    4. +
    5. Select Keep anyway.
    6. +

    For Firefox and Chrome, typically no additional action is needed. @@ -53,25 +53,26 @@

    To handle false warnings in Microsoft Defender: -

      -
    1. - Open Virus & threat protection from - the Start menu. -
    2. -
    3. - Locate the event in Protection history - that pertains to the script. -
    4. -
    5. In the event details, select Actions > Allow.
    6. -
    7. If the script was deleted, please re-download it.
    8. -

    +
      +
    1. + Open Virus & threat protection from + the Start menu. +
    2. +
    3. + Locate the event in Protection history + that pertains to the script. +
    4. +
    5. In the event details, select Actions > Allow.
    6. +
    7. If the script was deleted, please re-download it.
    8. +
    - Caution: For your security, remember to: + Caution: +

    For your security, remember to

      -
    • Only allow scripts from trusted sources.
    • -
    • Avoid broad exclusions in your antivirus settings.
    • -
    • Keep real-time protection enabled whenever possible.
    • +
    • only allow scripts from trusted sources,
    • +
    • avoid broad exclusions in your antivirus settings,
    • +
    • and keep real-time protection enabled whenever possible.
    diff --git a/src/presentation/components/DevToolkit/DevToolkit.vue b/src/presentation/components/DevToolkit/DevToolkit.vue index 0b2de468..f7d226d6 100644 --- a/src/presentation/components/DevToolkit/DevToolkit.vue +++ b/src/presentation/components/DevToolkit/DevToolkit.vue @@ -1,24 +1,27 @@ @@ -91,10 +94,6 @@ interface DevAction { display:flex; flex-direction: column; - hr { - width: 100%; - } - .toolkit-header { display:flex; flex-direction: row; @@ -108,7 +107,6 @@ interface DevAction { } .title { - font-weight: bold; text-align: center; } @@ -116,8 +114,11 @@ interface DevAction { display: flex; flex-direction: column; gap: 10px; + @include reset-ul; + + .action-button { + @include reset-button; - button { display: block; padding: 5px 10px; background-color: $color-primary; diff --git a/src/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.vue b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.vue index e62e7dfb..d9991a0a 100644 --- a/src/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.vue +++ b/src/presentation/components/Scripts/Menu/Recommendation/RecommendationDocumentation.vue @@ -1,53 +1,51 @@ diff --git a/tests/unit/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.spec.ts b/tests/unit/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.spec.ts index 47cbe16c..332e321d 100644 --- a/tests/unit/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.spec.ts +++ b/tests/unit/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.spec.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; -import { shallowMount } from '@vue/test-utils'; -import CodeInstruction from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue'; +import { VueWrapper, shallowMount } from '@vue/test-utils'; +import { ComponentPublicInstance } from 'vue'; +import CopyableCommand from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue'; import { expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync'; import { InjectionKeys } from '@/presentation/injectionSymbols'; import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard'; @@ -9,10 +10,10 @@ import { ClipboardStub } from '@tests/unit/shared/Stubs/ClipboardStub'; import { expectExists } from '@tests/shared/Assertions/ExpectExists'; import FlatButton from '@/presentation/components/Shared/FlatButton.vue'; -const DOM_SELECTOR_CODE_SLOT = 'code'; +const DOM_SELECTOR_CODE_SLOT = 'code > span:nth-of-type(2)'; const COMPONENT_TOOLTIP_WRAPPER_NAME = 'TooltipWrapper'; -describe('CodeInstruction.vue', () => { +describe('CopyableCommand.vue', () => { it('renders a slot content inside a element', () => { // arrange const expectedSlotContent = 'Example Code'; @@ -33,7 +34,8 @@ describe('CodeInstruction.vue', () => { const wrapper = mountComponent({ clipboard: clipboardStub, }); - wrapper.vm.codeElement = { textContent: expectedCode } as HTMLElement; + const referencedElement = createElementWithTextContent(expectedCode); + setSlotContainerElement(wrapper, referencedElement); // act const copyButton = wrapper.findComponent(FlatButton); await copyButton.trigger('click'); @@ -45,21 +47,23 @@ describe('CodeInstruction.vue', () => { const [actualCode] = call.args; expect(actualCode).to.equal(expectedCode); }); - it('throws an error when codeElement is not found during copy', async () => { + it('throws an error when referenced element is undefined during copy', async () => { // arrange const expectedError = 'Code element could not be found.'; const wrapper = mountComponent(); - wrapper.vm.codeElement = undefined; + const referencedElement = undefined; + setSlotContainerElement(wrapper, referencedElement); // act const act = () => wrapper.vm.copyCode(); // assert await expectThrowsAsync(act, expectedError); }); - it('throws an error when codeElement has no textContent during copy', async () => { + it('throws an error when reference element has no textContent during copy', async () => { // arrange const expectedError = 'Code element does not contain any text.'; const wrapper = mountComponent(); - wrapper.vm.codeElement = { textContent: '' } as HTMLElement; + const referencedElement = createElementWithTextContent(''); + setSlotContainerElement(wrapper, referencedElement); // act const act = () => wrapper.vm.copyCode(); // assert @@ -72,7 +76,7 @@ function mountComponent(options?: { readonly clipboard?: Clipboard, readonly slotContent?: string, }) { - return shallowMount(CodeInstruction, { + return shallowMount(CopyableCommand, { global: { provide: { [InjectionKeys.useClipboard.key]: @@ -95,3 +99,16 @@ function mountComponent(options?: { }, }); } + +function setSlotContainerElement( + wrapper: VueWrapper, + element?: HTMLElement, +) { + (wrapper.vm as ComponentPublicInstance).copyableTextHolder = element; +} + +function createElementWithTextContent(textContent: string): HTMLElement { + const element = document.createElement('span'); + element.textContent = textContent; + return element; +}