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

cofacts / rumors-site / 4050999257

pending completion
4050999257

Pull #500

github

GitHub
Merge 42bb5f89c into bf0362a6f
Pull Request #500: Initialize Typescript

386 of 603 branches covered (64.01%)

Branch coverage included in aggregate %.

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

924 of 1112 relevant lines covered (83.09%)

12.92 hits per line

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

87.68
/lib/text.tsx
1
import React, { CSSProperties } from 'react';
2
import { withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
3
import {
4
  FragmentType,
5
  useFragment as getFragment,
6
} from '../typegen/fragment-masking';
7
import { graphql } from '../typegen';
8

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

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

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

31
  return result;
32✔
32
}
33

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

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

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

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

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

64
  return null;
×
65
}
66

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

275
/**
276
 * Processes Highlights object from rumors-api
277
 */
278
export function highlightSections(
279
  highlightFields: FragmentType<typeof HighlightFields>,
280
  classes: {
281
    highlight?: string;
282
    reference?: string;
283
    hyperlinks?: string;
284
  }
285
) {
286
  const { text, reference, hyperlinks } = getFragment(
4✔
287
    HighlightFields,
288
    highlightFields
289
  );
290

291
  const jsxElems = [];
4✔
292

293
  if (text) {
4✔
294
    jsxElems.push(...getMarkElems(text, classes.highlight));
2✔
295
  }
296

297
  if (reference) {
4✔
298
    jsxElems.push(
1✔
299
      <section className={classes.reference} key="reference-section">
300
        {getMarkElems(reference, classes.highlight)}
301
      </section>
302
    );
303
  }
304

305
  if (hyperlinks && hyperlinks.length) {
4✔
306
    jsxElems.push(
2✔
307
      ...hyperlinks.map(({ title, summary }, idx) => (
308
        <section
2✔
309
          className={classes.hyperlinks}
310
          key={`hyperlink-section-${idx}`}
311
        >
312
          {getMarkElems(`${title || ''} ${summary || ''}`, classes.highlight)}
4!
313
        </section>
314
      ))
315
    );
316
  }
317

318
  return jsxElems;
4✔
319
}
320

321
const formatter =
322
  Intl && typeof Intl.NumberFormat === 'function'
2!
323
    ? new Intl.NumberFormat()
324
    : null;
325

326
export function formatNumber(num: number) {
327
  return formatter ? formatter.format(num) : num.toString();
65!
328
}
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