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

MarkUsProject / Markus / 15338637915

30 May 2025 03:36AM UTC coverage: 91.96% (+0.008%) from 91.952%
15338637915

Pull #7525

github

web-flow
Merge 70c1bb175 into 3fa28962d
Pull Request #7525: Maintain font size in grading view

635 of 1373 branches covered (46.25%)

Branch coverage included in aggregate %.

6 of 8 new or added lines in 1 file covered. (75.0%)

19 existing lines in 1 file now uncovered.

41947 of 44932 relevant lines covered (93.36%)

117.55 hits per line

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

75.28
/app/javascript/Components/Result/text_viewer.jsx
1
import React from "react";
2
import Prism from "prismjs";
3

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

18
  getContentFromProps(props, state) {
19
    if (props.url) {
107✔
20
      return state.content;
87✔
21
    } else {
22
      return props.content;
20✔
23
    }
24
  }
25

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

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

37
  componentDidMount() {
38
    this.highlight_root = this.raw_content.current.parentNode;
13✔
39

40
    if (localStorage.getItem("text_viewer_font_size") !== null) {
13✔
41
      try {
9✔
42
        this.setState({font_size: Number(localStorage.getItem("text_viewer_font_size"))});
9✔
43
      } catch (error) {
NEW
44
        localStorage.removeItem("text_viewer_font_size");
×
NEW
45
        console.error("An error occurred:", error.message);
×
46
      }
47
    }
48

49
    // Fetch content from a URL if it is passed as a prop. The URL should point to plaintext data.
50
    if (this.props.url) {
13✔
51
      this.props.setLoadingCallback(true);
9✔
52
      this.fetchContent(this.props.url)
9✔
53
        .then(content =>
54
          this.setState({content: content}, () => this.props.setLoadingCallback(false))
6✔
55
        )
56
        .catch(error => {
57
          this.props.setLoadingCallback(false);
3✔
58
          if (error instanceof DOMException) return;
3!
59
          console.error(error);
3✔
60
        });
61
    }
62

63
    if (this.getContent()) {
13✔
64
      this.ready_annotations();
2✔
65
    }
66
  }
67

68
  fetchContent(url) {
69
    if (this.abortController) {
12✔
70
      // Stops ongoing fetch requests. It's ok to call .abort() after the fetch has already completed,
71
      // fetch simply ignores it.
72
      this.abortController.abort();
3✔
73
    }
74
    // Reinitialize the controller, because the signal can't be reused after the request has been aborted.
75
    this.abortController = new AbortController();
12✔
76

77
    return fetch(url, {signal: this.abortController.signal})
12✔
78
      .then(response => {
79
        if (response.status === 413) {
11✔
80
          const errorMessage = I18n.t("submissions.oversize_submission_file");
2✔
81
          this.props.setErrorMessageCallback(errorMessage);
2✔
82
          throw new Error(errorMessage);
2✔
83
        } else {
84
          return response.text();
9✔
85
        }
86
      })
87
      .then(content => content.replace(/\r?\n/gm, "\n"));
9✔
88
  }
89

90
  componentDidUpdate(prevProps, prevState) {
91
    if (this.props.url && this.props.url !== prevProps.url) {
27✔
92
      // The URL has updated, so the content needs to be fetched using the new URL.
93
      this.props.setLoadingCallback(true);
3✔
94
      this.fetchContent(this.props.url)
3✔
95
        .then(content =>
96
          this.setState({content: content}, () => {
3✔
97
            this.props.setLoadingCallback(false);
3✔
98
            this.postInitContent(prevProps, prevState);
3✔
99
          })
100
        )
101
        .catch(error => {
UNCOV
102
          this.props.setLoadingCallback(false);
×
UNCOV
103
          if (error instanceof DOMException) return;
×
UNCOV
104
          console.error(error);
×
105
        });
106
    } else {
107
      this.postInitContent(prevProps, prevState);
24✔
108
    }
109
  }
110

111
  postInitContent(prevProps, prevState) {
112
    const content = this.getContentFromProps(this.props, this.state);
27✔
113
    const prevContent = this.getContentFromProps(prevProps, prevState);
27✔
114

115
    if (content && (content !== prevContent || this.props.annotations !== prevProps.annotations)) {
27✔
116
      this.ready_annotations();
11✔
117
      this.setState({copy_success: false});
11✔
118
    } else if (this.props.focusLine !== prevProps.focusLine) {
16!
UNCOV
119
      this.scrollToLine(this.props.focusLine);
×
120
    }
121
    if (prevState.font_size !== this.state.font_size && this.highlight_root !== null) {
27✔
122
      this.highlight_root.style.fontSize = this.state.font_size + "em";
15✔
123
    }
124
  }
125

126
  /**
127
   * Post-processes text contents and display in three ways:
128
   *
129
   * 1. Apply syntax highlighting
130
   * 2. Display annotations
131
   * 3. Scroll to line numbered this.props.focusLine
132
   */
133
  ready_annotations = () => {
13✔
134
    this.run_syntax_highlighting();
13✔
135

136
    if (this.annotation_manager !== null) {
13!
UNCOV
137
      this.annotation_manager.annotation_text_displayer.hide();
×
138
    }
139

140
    this.highlight_root.style.font_size = this.state.fontSize + "em";
13✔
141

142
    if (this.props.resultView) {
13!
UNCOV
143
      window.annotation_type = ANNOTATION_TYPES.CODE;
×
144

UNCOV
145
      window.annotation_manager = new TextAnnotationManager(this.raw_content.current.children);
×
UNCOV
146
      this.annotation_manager = window.annotation_manager;
×
147
    }
148

149
    this.props.annotations.forEach(this.display_annotation);
13✔
150
    this.scrollToLine(this.props.focusLine);
13✔
151
  };
152

153
  run_syntax_highlighting = () => {
13✔
154
    Prism.highlightElement(this.raw_content.current, false);
13✔
155
    let nodeLines = [];
13✔
156
    let currLine = document.createElement("span");
13✔
157
    currLine.classList.add("source-line");
13✔
158
    let currChildren = [];
13✔
159
    for (let node of this.raw_content.current.childNodes) {
13✔
160
      // Note: SourceCodeLine.glow assumes text nodes are wrapped in <span> elements
161
      let textContainer = document.createElement("span");
38✔
162
      let className = node.nodeType === Node.TEXT_NODE ? "" : node.className;
38✔
163
      textContainer.className = className;
38✔
164

165
      const splits = node.textContent.split("\n");
38✔
166
      for (let i = 0; i < splits.length - 1; i++) {
38✔
167
        textContainer.textContent = splits[i] + "\n";
2✔
168
        currLine.append(...currChildren, textContainer);
2✔
169
        nodeLines.push(currLine);
2✔
170
        currLine = document.createElement("span");
2✔
171
        currLine.classList.add("source-line");
2✔
172
        currChildren = [];
2✔
173
        textContainer = document.createElement("span");
2✔
174
        textContainer.className = className;
2✔
175
      }
176

177
      textContainer.textContent = splits[splits.length - 1];
38✔
178
      currLine.append(...currChildren, textContainer);
38✔
179
    }
180
    if (currLine.textContent.length > 0) {
13✔
181
      nodeLines.push(currLine);
12✔
182
    }
183
    this.raw_content.current.replaceChildren(
13✔
184
      ...nodeLines,
185
      this.raw_content.current.lastChild.cloneNode(true)
186
    );
187
  };
188

189
  change_font_size = delta => {
13✔
190
    let font_size = Math.max(this.state.font_size + delta, 0.25);
1✔
191
    this.setState({font_size: font_size});
1✔
192
    localStorage.setItem("text_viewer_font_size", font_size);
1✔
193
  };
194

195
  display_annotation = annotation => {
13✔
196
    let content;
UNCOV
197
    if (!annotation.deduction) {
×
UNCOV
198
      content = annotation.content;
×
199
    } else {
UNCOV
200
      content =
×
201
        annotation.content + " [" + annotation.criterion_name + ": -" + annotation.deduction + "]";
202
    }
203

204
    if (annotation.is_remark) {
×
UNCOV
205
      content += ` (${I18n.t("results.annotation.remark_flag")})`;
×
206
    }
207

208
    this.annotation_manager.addAnnotation(
×
209
      annotation.annotation_text_id,
210
      content,
211
      {
212
        start: parseInt(annotation.line_start, 10),
213
        end: parseInt(annotation.line_end, 10),
214
        column_start: parseInt(annotation.column_start, 10),
215
        column_end: parseInt(annotation.column_end, 10),
216
      },
217
      annotation.id,
218
      annotation.is_remark
219
    );
220
  };
221

222
  // Scroll to display the given line.
223
  scrollToLine = lineNumber => {
13✔
224
    if (this.highlight_root === null || lineNumber === undefined || lineNumber === null) {
13!
225
      return;
13✔
226
    }
227

UNCOV
228
    const line = this.highlight_root.querySelector(`span.source-line:nth-of-type(${lineNumber})`);
×
UNCOV
229
    if (line) {
×
UNCOV
230
      line.scrollIntoView();
×
231
    }
232
  };
233

234
  copyToClipboard = () => {
13✔
UNCOV
235
    const content = this.getContent();
×
236

237
    // Prevents from copying `null` or `undefined` to the clipboard. An empty string is ok to copy.
UNCOV
238
    if (content || content === "") {
×
239
      navigator.clipboard.writeText(content).then(() => {
×
UNCOV
240
        this.setState({copy_success: true});
×
241
      });
242
    } else {
243
      console.warn(`Tried to copy content with value ${content} to the clipboard.`);
×
244
      this.setState({copy_success: false});
×
245
    }
246
  };
247

248
  render() {
249
    const preElementName = `code-${this.props.submission_file_id}`;
40✔
250

251
    return (
40✔
252
      <React.Fragment>
253
        <div className="toolbar">
254
          <div className="toolbar-actions">
255
            <a href="#" onClick={this.copyToClipboard}>
256
              {this.state.copy_success ? "✔ " : ""}
40!
257
              {I18n.t("results.copy_text")}
258
            </a>
259
            <a href="#" onClick={() => this.change_font_size(0.25)}>
1✔
260
              +A
261
            </a>
UNCOV
262
            <a href="#" onClick={() => this.change_font_size(-0.25)}>
×
263
              -A
264
            </a>
265
          </div>
266
        </div>
267
        <pre name={preElementName} className={`line-numbers`}>
268
          <code ref={this.raw_content} className={`language-${this.props.type}`}>
269
            {this.getContent()}
270
          </code>
271
        </pre>
272
      </React.Fragment>
273
    );
274
  }
275
}
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