Skip to content

Commit

Permalink
feat: support "start" attribute in ol and ul elements
Browse files Browse the repository at this point in the history
This commit is a substantial refactor of lists, which now use
@jsamr/react-native-li library to display markers with wide and rich
generation algorithms.

fix #336
  • Loading branch information
jsamr committed Jun 4, 2021
1 parent 01415f4 commit b9d3154
Show file tree
Hide file tree
Showing 17 changed files with 413 additions and 431 deletions.
98 changes: 0 additions & 98 deletions packages/render-html/src/elements/GenericListElement.tsx

This file was deleted.

163 changes: 163 additions & 0 deletions packages/render-html/src/elements/ListElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/* eslint-disable react-native/no-inline-styles */
import { Text, View, ViewStyle } from 'react-native';
import React from 'react';
import { TBlock, TNode } from '@native-html/transient-render-engine';
import {
MarkedListItem,
useMarkedList,
MarkerBoxProps
} from '@jsamr/react-native-li';
import { DefaultTagRendererProps, TChildProps } from '../shared-types';
import { useTChildrenRenderer } from '../context/TChildrenRendererContext';
import { DEFAULT_TEXT_COLOR } from '../constants';
import pick from 'ramda/src/pick';
import defaultMarkers, { UnitaryListStyleSpec } from './defaultMarkers';
import { SupportedListStyleType } from './list-types';

export interface ListElementProps<T extends 'ol' | 'ul'>
extends DefaultTagRendererProps<TBlock> {
listType: T;
getListStyleTypeFromNestLevel: (nestLevel: number) => SupportedListStyleType;
getStyleFromNestLevel?: (nestLevel: number) => ViewStyle | null;
}

function getStartIndex(tnode: TNode) {
const parsedIndex = tnode.attributes.start
? Number(tnode.attributes.start)
: Number.NaN;
return Number.isNaN(parsedIndex) ? 1 : parsedIndex;
}

function createSymbolicMarkerRenderer(
component: UnitaryListStyleSpec['Component']
) {
return ({
style,
markerTextStyle,
counterRenderer,
markerTextWidth,
rtlMarkerReversed = false
}: MarkerBoxProps) => {
const prefix = counterRenderer.renderPrefix();
const suffix = counterRenderer.renderSuffix();
return (
<View
style={[
style,
{
flexDirection: rtlMarkerReversed ? 'row-reverse' : 'row',
justifyContent: 'flex-end',
width: markerTextWidth
}
]}>
{prefix !== '' && <Text style={markerTextStyle}>{prefix}</Text>}
{React.createElement(component, markerTextStyle as any)}
{suffix !== '' && <Text style={markerTextStyle}>{suffix}</Text>}
</View>
);
};
}

function extractMarkerTextStyle(tnode: TNode) {
return Object.assign(
{},
{
lineHeight: 14 * 1.3,
fontSize: 14,
color: DEFAULT_TEXT_COLOR
},
pick(
[
'fontStyle',
'fontWeight',
'fontFamily',
'fontVariant',
'color',
'lineHeight'
],
tnode.styles.nativeTextFlow
)
);
}

export default function ListElement({
tnode,
TDefaultRenderer,
listType,
style,
getListStyleTypeFromNestLevel,
getStyleFromNestLevel,
markers,
...props
}: ListElementProps<any>) {
const nestLevel =
listType === 'ol' ? markers.olNestLevel : markers.ulNestLevel;
const TChildrenRenderer = useTChildrenRenderer();
const rtl =
tnode.styles.nativeBlockFlow.direction === 'rtl' ||
markers.direction === 'rtl';
const nestLevelStyle = getStyleFromNestLevel?.call(null, nestLevel);
const selectedListType = getListStyleTypeFromNestLevel(nestLevel);
const listStyleType =
(tnode.styles.webTextFlow.listStyleType as SupportedListStyleType) ||
selectedListType;
if (__DEV__ && !(listStyleType in defaultMarkers)) {
console.warn(
`list-style-type "${listStyleType}" is not handled by react-native-render-html.` +
'You can register a custom list marker renderer with the appropriate prop.'
);
}
const spec =
listStyleType in defaultMarkers
? defaultMarkers[listStyleType]
: defaultMarkers[selectedListType];
const counterRenderer = spec.counterStyleRenderer;
const startIndex = getStartIndex(tnode);
const markerTextStyle = extractMarkerTextStyle(tnode);
const itemProps = useMarkedList({
counterRenderer,
startIndex,
markerTextStyle,
rtlLineReversed: rtl,
rtlMarkerReversed: rtl,
length: tnode.children.length,
renderMarker:
spec.type === 'cyclic'
? createSymbolicMarkerRenderer(spec.Component)
: undefined
});
const markerWidth = itemProps.markerTextWidth;
const renderChild = ({ childElement, key, index }: TChildProps) => (
<MarkedListItem
key={key}
index={index}
{...itemProps}
style={[itemProps.style, { [rtl ? 'right' : 'left']: -markerWidth }]}>
<View style={{ flexShrink: 1 }}>{childElement}</View>
</MarkedListItem>
);
const fixedPaddingRule = rtl
? ('paddingRight' as const)
: ('paddingLeft' as const);
const paddingValue = tnode.styles.nativeBlockRet[fixedPaddingRule];
const dynamicStyle = {
position: 'relative' as const,
[fixedPaddingRule]:
typeof paddingValue === 'number'
? Math.max(paddingValue, markerWidth)
: markerWidth
};
return (
<TDefaultRenderer
tnode={tnode}
markers={markers}
style={[style, nestLevelStyle, dynamicStyle]}
{...props}>
<TChildrenRenderer
tchildren={tnode.children}
renderChild={renderChild}
parentMarkers={markers}
/>
</TDefaultRenderer>
);
}
8 changes: 3 additions & 5 deletions packages/render-html/src/elements/OLElement.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React from 'react';
import GenericListElement, {
GenericListElementProps
} from './GenericListElement';
import ListElement, { ListElementProps } from './ListElement';

export type OLElementProps = Omit<GenericListElementProps<'ol'>, 'listType'>;
export type OLElementProps = Omit<ListElementProps<'ol'>, 'listType'>;
export default function OLElement(props: OLElementProps) {
return React.createElement(GenericListElement, { ...props, listType: 'ol' });
return React.createElement(ListElement, { ...props, listType: 'ol' });
}
9 changes: 4 additions & 5 deletions packages/render-html/src/elements/ULElement.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React from 'react';
import GenericListElement, {
GenericListElementProps
} from './GenericListElement';
import ListElement, { ListElementProps } from './ListElement';

export type ULElementProps = Omit<ListElementProps<'ul'>, 'listType'>;

export type ULElementProps = Omit<GenericListElementProps<'ul'>, 'listType'>;
export default function ULElement(props: ULElementProps) {
return React.createElement(GenericListElement, { ...props, listType: 'ul' });
return React.createElement(ListElement, { ...props, listType: 'ul' });
}

This file was deleted.

99 changes: 99 additions & 0 deletions packages/render-html/src/elements/defaultMarkers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import CounterStyle, { CounterStyleRenderer } from '@jsamr/counter-style';
import { ComponentType } from 'react';
import decimal from '@jsamr/counter-style/presets/decimal';
import decimalLeadingZero from '@jsamr/counter-style/presets/decimalLeadingZero';
import lowerAlpha from '@jsamr/counter-style/presets/lowerAlpha';
import lowerGreek from '@jsamr/counter-style/presets/lowerGreek';
import upperAlpha from '@jsamr/counter-style/presets/upperAlpha';
import DisclosureClosedSymbolRenderer from './symbolic/DisclosureClosedSymbolRenderer';
import DisclosureOpenSymbolRenderer from './symbolic/DisclosureOpenSymbolRenderer';
import CircleSymbolRenderer from './symbolic/CircleSymbolRenderer';
import DiscSymbolRenderer from './symbolic/DiscSymbolRenderer';
import SquareSymbolRenderer from './symbolic/SquareSymbolRenderer';
import { ListPrefixRendererProps, SupportedListStyleType } from './list-types';

const symbolicRenderer = CounterStyle.cyclic('*').withSuffix(' ');

/**
* Specs for a list item marker renderer backed by a `CounterStyleRenderer`
* from `@jsamr/counter-style`.
*/
export interface TextualListStyleSpec {
type: 'textual';
counterStyleRenderer: CounterStyleRenderer;
}

/**
* Specs for a list item marker renderer with only one representation. The
* "Component" should render this representation, minus prefix and suffix. The
* rendered component should have a maximum width of `0.6 * fontSize`, and a height of
* `lineHeight`.
*/
export interface UnitaryListStyleSpec {
counterStyleRenderer: CounterStyleRenderer;
type: 'cyclic';
Component: ComponentType<ListPrefixRendererProps>;
}

export type ListStyleSpec = TextualListStyleSpec | UnitaryListStyleSpec;

const lowerAlphaSpec = {
type: 'textual',
counterStyleRenderer: lowerAlpha
} as const;

const upperAlphaSpec = {
type: 'textual',
counterStyleRenderer: upperAlpha
} as const;

const defaultMarkers: Record<SupportedListStyleType, ListStyleSpec> = {
'decimal-leading-zero': {
type: 'textual',
counterStyleRenderer: decimalLeadingZero
},
'disclosure-closed': {
counterStyleRenderer: symbolicRenderer,
type: 'cyclic',
Component: DisclosureClosedSymbolRenderer
},
'disclosure-open': {
counterStyleRenderer: symbolicRenderer,
type: 'cyclic',
Component: DisclosureOpenSymbolRenderer
},
'lower-alpha': lowerAlphaSpec,
'lower-greek': {
type: 'textual',
counterStyleRenderer: lowerGreek
},
'lower-latin': lowerAlphaSpec,
'upper-alpha': upperAlphaSpec,
'upper-latin': upperAlphaSpec,
circle: {
counterStyleRenderer: symbolicRenderer,
type: 'cyclic',
Component: CircleSymbolRenderer
},
decimal: {
type: 'textual',
counterStyleRenderer: decimal
},
disc: {
counterStyleRenderer: symbolicRenderer,
type: 'cyclic',
Component: DiscSymbolRenderer
},
none: {
counterStyleRenderer: CounterStyle.symbolic('').withSuffix(null),
type: 'cyclic',
Component: () => null
},
square: {
counterStyleRenderer: symbolicRenderer,
type: 'cyclic',
Component: SquareSymbolRenderer
}
};

export default defaultMarkers;
Loading

0 comments on commit b9d3154

Please sign in to comment.