• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cofacts / rumors-site / 4076408061

pending completion
4076408061

push

github

GitHub
Merge pull request #500 from cofacts/typescript

387 of 604 branches covered (64.07%)

Branch coverage included in aggregate %.

86 of 86 new or added lines in 5 files covered. (100.0%)

925 of 1113 relevant lines covered (83.11%)

12.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.77
/lib/text.tsx
1
import React, { CSSProperties } from 'react';
2
import { gql } from 'graphql-tag';
3
import { print } from 'graphql';
4
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
5
import { FragmentType, getFragmentData } from '../typegen/fragment-masking';
6
import { graphql } from '../typegen';
7

8
const BREAK = { $$BREAK: true } as const;
2✔
9

10
/**
11
 * Called when `traverseForStrings()` reaches a string.
12
 */
13
type Callback = (s: string) => React.ReactNode | typeof BREAK;
14

15
/**
16
 * Invokes traverseForStrings for each item in elems.
17
 * When `BREAK` is received, break traversal immediately.
18
 *
19
 * @param elems - Array of elements to traverse
20
 * @param callback - passed to traverseForStrings()
21
 */
22
function traverseElems(elems: React.ReactNode[], callback: Callback) {
23
  const result = [];
32✔
24
  for (let i = 0; i < elems.length; i += 1) {
32✔
25
    const returnValue = traverseForStrings(elems[i], callback);
64✔
26
    if (returnValue === BREAK) break;
64✔
27
    result.push(returnValue);
54✔
28
  }
29

30
  return result;
32✔
31
}
32

33
/**
34
 * Traverses elem tree for strings, returns callback(string)
35
 * @param elem
36
 * @param callback
37
 */
38
function traverseForStrings(elem: React.ReactNode, callback: Callback) {
39
  if (typeof elem === 'string') {
90✔
40
    return callback(elem);
58✔
41
  }
42

43
  if (elem instanceof Array) {
32✔
44
    return traverseElems(elem, callback);
4✔
45
  }
46

47
  if (React.isValidElement(elem)) {
28!
48
    const children = React.Children.toArray(elem.props.children);
28✔
49
    const newChildren = traverseElems(children, callback);
28✔
50

51
    // No need to clone element if new children is identical with the original
52
    //
53
    if (
28✔
54
      children.length === newChildren.length &&
46✔
55
      children.every((child, idx) => child === newChildren[idx])
23✔
56
    ) {
57
      return elem;
11✔
58
    }
59

60
    return React.cloneElement(elem, {}, newChildren);
17✔
61
  }
62

63
  return null;
×
64
}
65

66
type CropperProps = {
67
  maxWidth: CSSProperties['maxWidth'];
68
  children: React.ReactNode;
69
};
70

71
const cropperStyle = createStyles({
2✔
72
  cropper: {
73
    display: 'inline-block',
74
    maxWidth: ({ maxWidth }: CropperProps) => `min(100%, ${maxWidth})`, // 100% ensures this inline-block does not stick out its container
×
75
    overflow: 'hidden',
76
    whiteSpace: 'nowrap',
77
    textOverflow: 'ellipsis',
78
    textDecoration: 'inherit',
79
    verticalAlign: 'bottom', // align with the rest of the URLs
80
  },
81
});
82

83
const Cropper = withStyles(cropperStyle)(
2✔
84
  ({ children, classes }: CropperProps & WithStyles<typeof cropperStyle>) => {
85
    return <span className={classes.cropper}>{children}</span>;
×
86
  }
87
);
88

89
function shortenUrl(s: string, maxLength: number) {
90
  try {
8✔
91
    s = decodeURIComponent(s);
8✔
92
  } catch (e) {
93
    // Probably malformed URI components.
94
    // Do nothing, just use original string
95
  }
96
  return s.length <= maxLength ? (
8✔
97
    s
98
  ) : (
99
    <>
100
      <Cropper maxWidth={`${maxLength / 4}em`}>
101
        {s.slice(0, -maxLength / 2)}
102
      </Cropper>
103
      {s.slice(-maxLength / 2)}
104
    </>
105
  );
106
}
107

108
function flatternPureStrings(tokens: React.ReactChild[]) {
109
  return tokens.every(token => typeof token === 'string')
42✔
110
    ? tokens.join()
111
    : tokens;
112
}
113

114
const urlRegExp = /(https?:\/\/\S+)/;
2✔
115

116
/**
117
 * Wrap <a> around hyperlinks inside a react element or string.
118
 *
119
 * @param elem React element, string, array of string & react elements
120
 * @param options
121
 */
122
export function linkify(
123
  elem: React.ReactNode,
124
  {
9✔
125
    maxLength = 80,
10✔
126
    props = {},
10✔
127
  }: { maxLength?: number; props?: React.ComponentPropsWithoutRef<'a'> } = {}
128
) {
129
  return traverseForStrings(elem, str => {
11✔
130
    if (!str) return str;
18!
131

132
    const tokenized = str.split(urlRegExp).map((s, i) =>
18✔
133
      s.match(urlRegExp) ? (
34✔
134
        <a key={`link${i}`} href={s} {...props}>
135
          {shortenUrl(s, maxLength)}
136
        </a>
137
      ) : (
138
        s
139
      )
140
    );
141

142
    return flatternPureStrings(tokenized);
18✔
143
  });
144
}
145

146
const newLinePattern = '(\r\n|\r|\n)';
2✔
147
// Spaces around new line pattern should be safe to trim, because we are placing <br>
148
// on the newLinePattern.
149
const newLineRegExp = RegExp(` *${newLinePattern} *`, 'g');
2✔
150

151
/**
152
 * Place <br> for each line break.
153
 * Automatically trims away leading & trailing line breaks.
154
 *
155
 * @param elem React element, string, array of string & react elements
156
 */
157
export function nl2br(elem: React.ReactNode) {
158
  return traverseForStrings(elem, str => {
8✔
159
    if (!str) return str;
13!
160

161
    const tokenized = str
13✔
162
      .split(newLineRegExp)
163
      .filter(token => token !== '') // Filter out empty strings
25✔
164
      .map((line, idx) =>
165
        line.match(newLineRegExp) ? <br key={`br${idx}`} /> : line
22✔
166
      );
167

168
    // If the tokenized contains only string, join into one single string.
169
    //
170
    return flatternPureStrings(tokenized);
13✔
171
  });
172
}
173

174
export function ellipsis(
175
  /** Text to ellipsis */
176
  text: string,
177
  {
×
178
    wordCount = Infinity,
×
179
    morePostfix = '⋯⋯',
2✔
180
  }: {
181
    /** Max length of the text before it's being ellipsised */
182
    wordCount?: number;
183

184
    /** The ellipsis postfix. Default to '⋯⋯' */
185
    morePostfix?: string;
186
  } = {}
187
) {
188
  if (text.length <= wordCount) {
2!
189
    return text;
2✔
190
  }
191

192
  return `${text.slice(0, wordCount)}${morePostfix}`;
×
193
}
194

195
/**
196
 * Truncates the given elem to the specified `wordCount`.
197
 * When truncated, appends `moreElem` to the end of the string.
198
 *
199
 * @param elem - React element, string, array of string & react elements
200
 * @param options
201
 */
202
export function truncate(
203
  elem: React.ReactNode,
204
  {
1✔
205
    wordCount = Infinity,
1✔
206
    moreElem = null,
1✔
207
  }: {
208
    wordCount?: number;
209
    moreElem?: React.ReactElement;
210
  } = {}
211
) {
212
  let currentWordCount = 0;
7✔
213
  const result = traverseForStrings(elem, str => {
7✔
214
    if (currentWordCount >= wordCount) return BREAK;
27✔
215

216
    currentWordCount += str.length;
17✔
217
    const exceededCount = currentWordCount - wordCount;
17✔
218

219
    return exceededCount <= 0 ? str : str.slice(0, -exceededCount);
17✔
220
  });
221

222
  // Not exceeding wordCount, just return the original element
223
  if (currentWordCount <= wordCount) {
7✔
224
    return elem;
1✔
225
  }
226

227
  // If the result is an array, append moreElem
228
  if (result instanceof Array) {
6!
229
    return result.concat(moreElem);
×
230
  }
231

232
  // Others, including result being a string or React Element.
233
  return [result, moreElem];
6✔
234
}
235

236
/**
237
 * Converts highlight string (with "<HIGHLIGHT></HIGHLIGHT>") to JSX array of strings and <mark> elems
238
 */
239
function getMarkElems(
240
  highlightStr: string | null | undefined,
241
  highlightClassName: string
242
) {
243
  if (typeof highlightStr !== 'string') return [];
5!
244

245
  return highlightStr
5✔
246
    .split('</HIGHLIGHT>')
247
    .reduce<React.ReactChild[]>((tokens, token, idx) => {
248
      const [nonHighlight, highlighted] = token.split('<HIGHLIGHT>');
16✔
249

250
      return [
16✔
251
        ...tokens,
252
        nonHighlight,
253
        highlighted && (
27✔
254
          <mark key={`highlight-${idx}`} className={highlightClassName}>
255
            {highlighted}
256
          </mark>
257
        ),
258
      ];
259
    }, [])
260
    .filter(jsxElem => !!jsxElem);
32✔
261
}
262

263
const TypedHighlightFields = graphql(/* GraphQL */ `
2✔
264
  fragment HighlightFields on Highlights {
265
    text
266
    reference
267
    hyperlinks {
268
      title
269
      summary
270
    }
271
  }
272
`);
273

274
/**
275
 * @deprecated - Remove export after all dependent queries use codegen graphql() instead of graphql-tag gql.
276
 * @see https://stackoverflow.com/a/72802504/1582110
277
 */
278
export const HighlightFields = gql`
2✔
279
  ${print(TypedHighlightFields)}
280
`;
281

282
/**
283
 * Processes Highlights object from rumors-api
284
 */
285
export function highlightSections(
286
  highlightFields: FragmentType<typeof TypedHighlightFields>,
287
  classes: {
288
    highlight?: string;
289
    reference?: string;
290
    hyperlinks?: string;
291
  }
292
) {
293
  const { text, reference, hyperlinks } = getFragmentData(
4✔
294
    TypedHighlightFields,
295
    highlightFields
296
  );
297

298
  const jsxElems = [];
4✔
299

300
  if (text) {
4✔
301
    jsxElems.push(...getMarkElems(text, classes.highlight));
2✔
302
  }
303

304
  if (reference) {
4✔
305
    jsxElems.push(
1✔
306
      <section className={classes.reference} key="reference-section">
307
        {getMarkElems(reference, classes.highlight)}
308
      </section>
309
    );
310
  }
311

312
  if (hyperlinks && hyperlinks.length) {
4✔
313
    jsxElems.push(
2✔
314
      ...hyperlinks.map(({ title, summary }, idx) => (
315
        <section
2✔
316
          className={classes.hyperlinks}
317
          key={`hyperlink-section-${idx}`}
318
        >
319
          {getMarkElems(`${title || ''} ${summary || ''}`, classes.highlight)}
4!
320
        </section>
321
      ))
322
    );
323
  }
324

325
  return jsxElems;
4✔
326
}
327

328
const formatter =
329
  Intl && typeof Intl.NumberFormat === 'function'
2!
330
    ? new Intl.NumberFormat()
331
    : null;
332

333
export function formatNumber(num: number) {
334
  return formatter ? formatter.format(num) : num.toString();
65!
335
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc