Skip to content
This repository has been archived by the owner on Jan 30, 2024. It is now read-only.

Commit

Permalink
feat: initial implementation and some tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Noah Hummel committed Feb 24, 2022
0 parents commit 84bdc91
Show file tree
Hide file tree
Showing 15 changed files with 12,945 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea
node_modules
43 changes: 43 additions & 0 deletions lib/Trans.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useTranslation } from 'react-i18next';
import React, { Fragment, ReactElement } from 'react';
import { renderTreeFromText } from './renderTreeFromText';
import { PlaceholderFunction } from './types/PlaceholderFunction';

interface TransProps <TComponentNames extends string, TInterpolations extends string>{
children: PlaceholderFunction<TComponentNames, TInterpolations>;
namespace: string;
translation: string;
components: Record<TComponentNames, React.FunctionComponent>;
interpolations?: Record<TInterpolations, any>;
}

const Trans = function <TComponentNames extends string, TInterpolations extends string> ({
children,
translation,
namespace,
components,
interpolations
}: TransProps<TComponentNames, TInterpolations>): ReactElement {
const { t, i18n } = useTranslation(namespace);

const resource = i18n.getResource(i18n.language, namespace, translation);

if (!resource) {
return children(components, interpolations);
}

return (
<Fragment>
{
renderTreeFromText({
text: t(translation, interpolations),
components
})
}
</Fragment>
);
};

export {
Trans
};
15 changes: 15 additions & 0 deletions lib/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defekt } from 'defekt';

class ClosingTagDoesNotMatchOpeningTag extends defekt({ code: 'ClosingTagDoesNotMatchOpeningTag' }) {}
class NotAllTagsWereClosed extends defekt({ code: 'NotAllTagsWereClosed' }) {}
class TagIsIncomplete extends defekt({ code: 'TagIsIncomplete' }) {}
class TagIsNotKnown extends defekt({ code: 'TagIsNotKnown' }) {}
class TagNameIsInvalid extends defekt({ code: 'TagNameIsInvalid' }) {}

export {
ClosingTagDoesNotMatchOpeningTag,
NotAllTagsWereClosed,
TagIsIncomplete,
TagIsNotKnown,
TagNameIsInvalid
};
Empty file added lib/index.ts
Empty file.
171 changes: 171 additions & 0 deletions lib/renderTreeFromText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { ClosingTagDoesNotMatchOpeningTag, NotAllTagsWereClosed, TagIsIncomplete, TagIsNotKnown, TagNameIsInvalid } from './errors';
import { Tag } from './types/Tag';
import React, { ReactNode } from 'react';

const renderTreeFromText = ({
text,
components
}: {
text: string;
components: Record<string, React.FunctionComponent>;
}): ReactNode[] => {
const openTags: Tag[] = [
{
name: 'root',
children: []
}
];

let currentTextPart = '';

for (let i = 0; i < text.length;) {
const nextChar = text[i];
const remainingChars = text.length - i;

if (nextChar !== '<') {
currentTextPart += nextChar;
i += 1;

continue;
}

if (i + 1 === text.length) {
throw new TagIsIncomplete({ data: { position: i } });
}

const isClosingTag = text[i + 1] === '/';

if (!isClosingTag) {
const currentlyOpenTag = openTags.at(0);

if (currentTextPart.length > 0) {
currentlyOpenTag!.children.push(currentTextPart);
currentTextPart = '';
}

let tag = '';
let readChars = 0;

for (; readChars < remainingChars; readChars++) {
const nextTagChar = text[i + readChars];

tag += nextTagChar;

if (nextTagChar === '>') {
break;
}

if (readChars + 1 === remainingChars) {
throw new TagIsIncomplete({
data: {
position: i,
tag: tag.slice(1)
}
});
}
}

const tagName = tag.slice(1, -1);

if (!/^[a-zA-Z]\w*$/gm.test(tagName)) {
throw new TagNameIsInvalid({
data: {
tag: tagName,
position: i
}
});
}

openTags.unshift({
name: tagName,
children: []
});
i += readChars + 1;

continue;
}

let tag = '';
let readChars = 0;

for (; readChars < remainingChars; readChars++) {
const nextTagChar = text[i + readChars];

tag += nextTagChar;

if (nextTagChar === '>') {
break;
}

if (readChars + 1 === remainingChars) {
throw new TagIsIncomplete({
data: {
position: i,
tag: tag.slice(2)
}
});
}
}

const tagName = tag.slice(2, -1);
const closedTag = openTags.shift();

if (!/^[a-zA-Z]\w*$/gm.test(tagName)) {
throw new TagNameIsInvalid({
data: {
tag: tagName,
position: i
}
});
}

if (tagName !== closedTag!.name) {
throw new ClosingTagDoesNotMatchOpeningTag({
data: {
position: i,
tag: tagName,
openTags
}
});
}

if (currentTextPart.length > 0) {
closedTag!.children.push(currentTextPart);
currentTextPart = '';
}

const Component = components[tagName];

if (!Component) {
throw new TagIsNotKnown({
data: {
tag: tagName,
position: i
}
});
}

const renderedClosedTag = (<Component>{ ...closedTag!.children }</Component>);
const currentlyOpenTag = openTags.at(0);

currentlyOpenTag!.children.push(renderedClosedTag);

i += readChars + 1;
}

if (openTags.length > 1) {
throw new NotAllTagsWereClosed({ data: { openTags } });
}

const root = openTags.shift();

if (currentTextPart.length > 0) {
root!.children.push(currentTextPart);
}

return root!.children;
};

export {
renderTreeFromText
};
8 changes: 8 additions & 0 deletions lib/types/DumbComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FunctionComponent } from 'react';
import { NoProps } from './NoProps';

type DumbComponent = FunctionComponent<NoProps>;

export type {
DumbComponent
};
5 changes: 5 additions & 0 deletions lib/types/NoProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type NoProps = Record<any, unknown>;

export type {
NoProps
};
9 changes: 9 additions & 0 deletions lib/types/PlaceholderFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ReactElement } from 'react';
import { DumbComponent } from './DumbComponent';

type PlaceholderFunction <TComponentNames extends string, TInterpolations extends string>
= (components: Record<TComponentNames, DumbComponent>, interpolations?: Record<TInterpolations, any>) => ReactElement

export type {
PlaceholderFunction
};
10 changes: 10 additions & 0 deletions lib/types/Tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ReactNode } from 'react';

interface Tag {
name: string;
children: ReactNode[];
}

export type {
Tag
};
Loading

0 comments on commit 84bdc91

Please sign in to comment.