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

MarkUsProject / Markus / 12436096582

20 Dec 2024 05:53PM UTC coverage: 91.764% (+0.004%) from 91.76%
12436096582

Pull #7270

github

web-flow
Merge 492f7e32d into ea33a3f40
Pull Request #7270: Refactor file viewer

623 of 1357 branches covered (45.91%)

Branch coverage included in aggregate %.

114 of 148 new or added lines in 9 files covered. (77.03%)

2 existing lines in 2 files now uncovered.

41179 of 44197 relevant lines covered (93.17%)

120.52 hits per line

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

71.6
/app/javascript/Components/Result/text_viewer.jsx
1
import React from "react";
2
import {render} from "react-dom";
3
import Prism from "prismjs";
4

5
export class TextViewer extends React.PureComponent {
6
  constructor(props) {
7
    super(props);
11✔
8
    this.state = {
11✔
9
      copy_success: false,
10
      font_size: 1,
11
      content: null,
12
    };
13
    this.highlight_root = null;
11✔
14
    this.annotation_manager = null;
11✔
15
    this.raw_content = React.createRef();
11✔
16
    this.abortController = null;
11✔
17
  }
18

19
  getContentFromProps(props, state) {
20
    if (props.url) {
76✔
21
      return state.content;
72✔
22
    } else {
23
      return props.content;
4✔
24
    }
25
  }
26

27
  // Retrieves content used by the component.
28
  getContent() {
29
    return this.getContentFromProps(this.props, this.state);
40✔
30
  }
31

32
  componentWillUnmount() {
33
    if (this.abortController) {
9✔
34
      this.abortController.abort();
7✔
35
    }
36
  }
37

38
  componentDidMount() {
39
    this.highlight_root = this.raw_content.current.parentNode;
11✔
40

41
    // Fetch content from a URL if it is passed as a prop. The URL should point to plaintext data.
42
    if (this.props.url) {
11✔
43
      this.props.setLoadingCallback(true);
9✔
44
      this.fetchContent(this.props.url)
9✔
45
        .then(content =>
46
          this.setState({content: content}, () => this.props.setLoadingCallback(false))
6✔
47
        )
48
        .catch(error => {
49
          this.props.setLoadingCallback(false);
3✔
50
          if (error instanceof DOMException) return;
3!
51
          console.error(error);
3✔
52
        });
53
    }
54

55
    if (this.getContent()) {
11✔
56
      this.ready_annotations();
2✔
57
    }
58
  }
59

60
  fetchContent(url) {
61
    if (this.abortController) {
12✔
62
      // Stops ongoing fetch requests. It's ok to call .abort() after the fetch has already completed,
63
      // fetch simply ignores it.
64
      this.abortController.abort();
3✔
65
    }
66
    // Reinitialize the controller, because the signal can't be reused after the request has been aborted.
67
    this.abortController = new AbortController();
12✔
68

69
    return fetch(url, {signal: this.abortController.signal})
12✔
70
      .then(response => {
71
        if (response.status === 413) {
11✔
72
          const errorMessage = I18n.t("submissions.oversize_submission_file");
2✔
73
          this.props.setErrorMessageCallback(errorMessage);
2✔
74
          throw new Error(errorMessage);
2✔
75
        } else {
76
          return response.text();
9✔
77
        }
78
      })
79
      .then(content => content.replace(/\r?\n/gm, "\n"));
9✔
80
  }
81

82
  componentDidUpdate(prevProps, prevState) {
83
    if (this.props.url && this.props.url !== prevProps.url) {
18✔
84
      // The URL has updated, so the content needs to be fetched using the new URL.
85
      this.props.setLoadingCallback(true);
3✔
86
      this.fetchContent(this.props.url)
3✔
87
        .then(content =>
88
          this.setState({content: content}, () => {
3✔
89
            this.props.setLoadingCallback(false);
3✔
90
            this.postInitContent(prevProps, prevState);
3✔
91
          })
92
        )
93
        .catch(error => {
NEW
94
          this.props.setLoadingCallback(false);
×
NEW
95
          if (error instanceof DOMException) return;
×
NEW
96
          console.error(error);
×
97
        });
98
    } else {
99
      this.postInitContent(prevProps, prevState);
15✔
100
    }
101
  }
102

103
  postInitContent(prevProps, prevState) {
104
    const content = this.getContentFromProps(this.props, this.state);
18✔
105
    const prevContent = this.getContentFromProps(prevProps, prevState);
18✔
106

107
    if (content && (content !== prevContent || this.props.annotations !== prevProps.annotations)) {
18!
108
      this.ready_annotations();
6✔
109
      this.setState({copy_success: false});
6✔
110
    } else if (this.props.focusLine !== prevProps.focusLine) {
12!
111
      this.scrollToLine(this.props.focusLine);
×
112
    }
113
    if (prevState.font_size !== this.state.font_size && this.highlight_root !== null) {
18!
114
      this.highlight_root.style.fontSize = this.state.font_size + "em";
×
115
    }
116
  }
117

118
  /**
119
   * Post-processes text contents and display in three ways:
120
   *
121
   * 1. Apply syntax highlighting
122
   * 2. Display annotations
123
   * 3. Scroll to line numbered this.props.focusLine
124
   */
125
  ready_annotations = () => {
11✔
126
    this.run_syntax_highlighting();
8✔
127

128
    if (this.annotation_manager !== null) {
8!
129
      this.annotation_manager.annotation_text_displayer.hide();
×
130
    }
131

132
    this.highlight_root.style.font_size = this.state.fontSize + "em";
8✔
133

134
    if (this.props.resultView) {
8!
135
      window.annotation_type = ANNOTATION_TYPES.CODE;
×
136

137
      window.annotation_manager = new TextAnnotationManager(this.raw_content.current.children);
×
138
      this.annotation_manager = window.annotation_manager;
×
139
    }
140

141
    this.props.annotations.forEach(this.display_annotation);
8✔
142
    this.scrollToLine(this.props.focusLine);
8✔
143
  };
144

145
  run_syntax_highlighting = () => {
11✔
146
    Prism.highlightElement(this.raw_content.current, false);
8✔
147
    let nodeLines = [];
8✔
148
    let currLine = document.createElement("span");
8✔
149
    currLine.classList.add("source-line");
8✔
150
    let currChildren = [];
8✔
151
    for (let node of this.raw_content.current.childNodes) {
8✔
152
      // Note: SourceCodeLine.glow assumes text nodes are wrapped in <span> elements
153
      let textContainer = document.createElement("span");
16✔
154
      let className = node.nodeType === Node.TEXT_NODE ? "" : node.className;
16✔
155
      textContainer.className = className;
16✔
156

157
      const splits = node.textContent.split("\n");
16✔
158
      for (let i = 0; i < splits.length - 1; i++) {
16✔
159
        textContainer.textContent = splits[i] + "\n";
2✔
160
        currLine.append(...currChildren, textContainer);
2✔
161
        nodeLines.push(currLine);
2✔
162
        currLine = document.createElement("span");
2✔
163
        currLine.classList.add("source-line");
2✔
164
        currChildren = [];
2✔
165
        textContainer = document.createElement("span");
2✔
166
        textContainer.className = className;
2✔
167
      }
168

169
      textContainer.textContent = splits[splits.length - 1];
16✔
170
      currLine.append(...currChildren, textContainer);
16✔
171
    }
172
    if (currLine.textContent.length > 0) {
8✔
173
      nodeLines.push(currLine);
7✔
174
    }
175
    this.raw_content.current.replaceChildren(
8✔
176
      ...nodeLines,
177
      this.raw_content.current.lastChild.cloneNode(true)
178
    );
179
  };
180

181
  change_font_size = delta => {
11✔
182
    this.setState({font_size: Math.max(this.state.font_size + delta, 0.25)});
×
183
  };
184

185
  display_annotation = annotation => {
11✔
186
    let content;
187
    if (!annotation.deduction) {
×
188
      content = annotation.content;
×
189
    } else {
190
      content =
×
191
        annotation.content + " [" + annotation.criterion_name + ": -" + annotation.deduction + "]";
192
    }
193

194
    if (annotation.is_remark) {
×
195
      content += ` (${I18n.t("results.annotation.remark_flag")})`;
×
196
    }
197

198
    this.annotation_manager.addAnnotation(
×
199
      annotation.annotation_text_id,
200
      content,
201
      {
202
        start: parseInt(annotation.line_start, 10),
203
        end: parseInt(annotation.line_end, 10),
204
        column_start: parseInt(annotation.column_start, 10),
205
        column_end: parseInt(annotation.column_end, 10),
206
      },
207
      annotation.id,
208
      annotation.is_remark
209
    );
210
  };
211

212
  // Scroll to display the given line.
213
  scrollToLine = lineNumber => {
11✔
214
    if (this.highlight_root === null || lineNumber === undefined || lineNumber === null) {
8!
215
      return;
8✔
216
    }
217

218
    const line = this.highlight_root.querySelector(`span.source-line:nth-of-type(${lineNumber})`);
×
219
    if (line) {
×
220
      line.scrollIntoView();
×
221
    }
222
  };
223

224
  copyToClipboard = () => {
11✔
NEW
225
    const content = this.getContent();
×
226

227
    // Prevents from copying `null` or `undefined` to the clipboard. An empty string is ok to copy.
NEW
228
    if (content || content === "") {
×
NEW
229
      navigator.clipboard.writeText(content).then(() => {
×
NEW
230
        this.setState({copy_success: true});
×
231
      });
232
    } else {
NEW
233
      console.warn(`Tried to copy content with value ${content} to the clipboard.`);
×
NEW
234
      this.setState({copy_success: false});
×
235
    }
236
  };
237

238
  render() {
239
    const preElementName = `code-${this.props.submission_file_id}`;
29✔
240

241
    return (
29✔
242
      <React.Fragment>
243
        <div className="toolbar">
244
          <div className="toolbar-actions">
245
            <a href="#" onClick={this.copyToClipboard}>
246
              {this.state.copy_success ? "✔ " : ""}
29!
247
              {I18n.t("results.copy_text")}
248
            </a>
249
            <a href="#" onClick={() => this.change_font_size(0.25)}>
×
250
              +A
251
            </a>
252
            <a href="#" onClick={() => this.change_font_size(-0.25)}>
×
253
              -A
254
            </a>
255
          </div>
256
        </div>
257
        <pre name={preElementName} className={`line-numbers`}>
258
          <code ref={this.raw_content} className={`language-${this.props.type}`}>
259
            {this.getContent()}
260
          </code>
261
        </pre>
262
      </React.Fragment>
263
    );
264
  }
265
}
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

© 2026 Coveralls, Inc