Skip to content

Commit 6cd3b26

Browse files
authored
Merge pull request #859 from classtranscribe/fix/frontend-compilation-errors
fix: resolve frontend compilation errors and linter issues
2 parents 7e13370 + f6734bc commit 6cd3b26

File tree

13 files changed

+341
-179
lines changed

13 files changed

+341
-179
lines changed

public/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646

4747
<!-- KaTeX Styles -->
4848
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css" integrity="sha384-zB1R0rpPzHqg7Kpt0Aljp8JPLqbXI3bhnPWROx27a9N0Ll6ZP/+DiW/UqRcLbRjq" crossorigin="anonymous">
49+
<!-- KaTeX JavaScript -->
50+
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.js" crossorigin="anonymous"></script>
51+
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/contrib/auto-render.min.js" crossorigin="anonymous"></script>
4952
</head>
5053
<body>
5154
<noscript>You need to enable JavaScript to run this app.</noscript>

src/components/CTImagePickerModal/ImagesTab.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
33
import { CTText } from 'layout';
44
import { uurl } from 'utils/use-url';
55
import Image from 'components/Image';
6-
import _ from 'lodash';
76
import ImagePreview from './ImagePreview';
87

98
function ImagesTab(props) {
@@ -26,7 +25,14 @@ function ImagesTab(props) {
2625
className="ct-img-picker-img-con"
2726
data-current={img === imgUrl}
2827
onClick={() => setImgUrl(img)}
29-
role="listitem"
28+
onKeyDown={(e) => {
29+
if (e.key === 'Enter' || e.key === ' ') {
30+
e.preventDefault();
31+
setImgUrl(img);
32+
}
33+
}}
34+
role="button"
35+
aria-label={`Select image ${img}`}
3036
>
3137
<Image src={uurl.getMediaUrl(img)} alt="Chapter Cover" />
3238
<div className="ct-img-picker-img-wrapper ct-d-r-center">

src/components/CTMarkdown/MarkdownPreviewer/index.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useRef } from 'react';
22
import PropTypes from 'prop-types';
33
import cx from 'classnames';
44
import { html } from 'utils';
@@ -22,8 +22,28 @@ function MarkdownPreviewer(props) {
2222
...otherProps
2323
} = props;
2424

25+
const previewRef = useRef(null);
26+
2527
useEffect(() => {
2628
Prism.highlightAll();
29+
30+
// Render KaTeX math expressions if KaTeX is available
31+
if (typeof window !== 'undefined' && window.katex && window.renderMathInElement && previewRef.current) {
32+
try {
33+
window.renderMathInElement(previewRef.current, {
34+
delimiters: [
35+
{left: '$$', right: '$$', display: true},
36+
{left: '$', right: '$', display: false},
37+
{left: '\\[', right: '\\]', display: true},
38+
{left: '\\(', right: '\\)', display: false}
39+
],
40+
throwOnError: false
41+
});
42+
} catch (error) {
43+
// Silently fail if KaTeX rendering fails
44+
console.warn('KaTeX rendering error:', error);
45+
}
46+
}
2747
}, [value]);
2848

2949
const previewClasses = cx('ct-md', 'preview', className);
@@ -41,10 +61,10 @@ function MarkdownPreviewer(props) {
4161
...otherProps
4262
};
4363

44-
previewElement = <div {...previewProps} />;
64+
previewElement = <div {...previewProps} ref={previewRef} />;
4565
} else {
4666
previewElement = (
47-
<div id={id} className={htmlClasses} {...otherProps}>
67+
<div id={id} className={htmlClasses} ref={previewRef} {...otherProps}>
4868
{children}
4969
</div>
5070
);

src/entities/EPubs/html-converters.js

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import _ from 'lodash';
2-
import { html, uurl, _buildID } from 'utils';
2+
import { html, _buildID } from 'utils';
33
import EPubParser from 'screens/EPub/controllers/file-builders/EPubParser';
44

55
export function buildMDFromItems(items) {
@@ -12,36 +12,71 @@ export function buildMDFromItems(items) {
1212

1313
export async function buildMDFromContent(content) {
1414
if (typeof content === 'string') return content;
15+
1516
// unwrap __data__ for correct image loading in subchapters
1617
if ("__data__" in content) {
1718
content = JSON.parse(JSON.stringify(content.__data__))
1819
}
1920

20-
let img_data_url = null
21+
if (!content || !content.src) {
22+
console.warn('buildMDFromContent: Invalid content, missing src:', content);
23+
return '<div class="img-block"><p><em>Image not available</em></p></div>';
24+
}
25+
const src = content.src;
26+
27+
let img_data_url = null;
28+
let imgError = false;
29+
2130
try {
22-
const img = await EPubParser.loadImageBuffer(uurl.getMediaUrl(content.src));
31+
// loadImageBuffer expects the src and will call uurl.getMediaUrl internally
32+
const img = await EPubParser.loadImageBuffer(src);
33+
34+
// Check if image buffer is valid (not empty string)
35+
if (!img || (typeof img === 'string' && img === '')) {
36+
throw new Error('Empty image buffer returned');
37+
}
38+
2339
const img_blob = new Blob([img]);
2440
img_data_url = await EPubParser.blobToDataUrl(img_blob);
41+
42+
// Verify data URL was created
43+
if (!img_data_url || img_data_url === '') {
44+
throw new Error('Failed to create data URL from image blob');
45+
}
2546
} catch (error) {
26-
// intentionally ignoring error in fetching image.
27-
// Most likely cause is CORS policy when running local dev server
47+
imgError = true;
48+
console.warn('buildMDFromContent: Error loading image:', error, 'src:', src);
49+
// Try to use the original src as fallback
50+
img_data_url = src;
2851
}
2952

30-
let link_url = content.link;
53+
if (!img_data_url || img_data_url === '') {
54+
console.warn('buildMDFromContent: No valid image URL, using placeholder');
55+
img_data_url = '';
56+
}
57+
58+
let link_url = content.link || '';
3159
// if (content.timestamp) {
3260
// link_url = links.watch(epub.sourceId, { begin: TimeString.toSeconds(timestamp) })
3361
// }
3462

35-
if (img_data_url === null) {
36-
img_data_url = "";
37-
}
3863
let despId = _buildID();
64+
const descriptions = content.descriptions || [];
65+
const descriptionsHtml = descriptions.length !== 0
66+
? `\t<div id="${despId}">${html.markdown(descriptions.join("\n"))}</div>`
67+
: '';
68+
69+
// If image failed to load, show error message
70+
const imgTag = imgError && !img_data_url
71+
? `\t<img src="" alt="${content.alt || 'Image not available'}" aria-describedby="${despId}" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';" />\n\t<p style="display:none; color: red;"><em>Image failed to load: ${content.src}</em></p>`
72+
: `\t<img src="${img_data_url}" alt="${content.alt || ''}" aria-describedby="${despId}" />`;
73+
3974
return [
4075
'<div class="img-block">',
4176
(link_url && link_url !== "") ? `<a href="${link_url}">` : "",
42-
`\t<img src="${img_data_url}" alt="${content.alt}" aria-describedby="${despId}" />`,
77+
imgTag,
4378
(link_url && link_url !== "") ? `</a>` : "",
44-
content.descriptions.length !== 0 ? `\t<div id="${despId}">${html.markdown(content.descriptions.join("\n"))}</div>` : "",
79+
descriptionsHtml,
4580
'</div>'
4681
].join('\n');
4782
}

src/layout/CTLoader/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { React, Fragment } from 'react';
1+
import React from 'react';
22
import PropTypes from 'prop-types';
33
// import CTFragment from '../CTFragment';
44
import './index.css';

src/screens/EPub/components/EPubHeader/ToolButton.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,27 @@ import IconButton from '@material-ui/core/IconButton';
33
import Button from '@material-ui/core/Button';
44
import { CTPopoverLabel, altEl } from 'layout';
55

6-
function ToolButton({
7-
onClick,
8-
icon,
9-
label,
10-
shortcut,
11-
anchorRef,
12-
...otherProps
13-
}) {
6+
function ToolButton({ onClick, icon, label, shortcut, anchorRef, active, ...otherProps }) {
147
const fullLabel = shortcut ? `${label} (${shortcut})` : label;
8+
const buttonClassName = active
9+
? 'ct-epb toolbar-btn icon-btn active'
10+
: 'ct-epb toolbar-btn icon-btn';
11+
const buttonClassNameText = active ? 'ct-epb toolbar-btn active' : 'ct-epb toolbar-btn';
12+
13+
// Remove active from otherProps to prevent passing it to Material-UI components
14+
const { active: _, ...propsWithoutActive } = otherProps;
1515

1616
return (
1717
<CTPopoverLabel label={fullLabel}>
1818
{icon ? (
1919
<IconButton
2020
onClick={onClick}
2121
id={`ct-epb-h-toolbth-${icon}`}
22-
className="ct-epb toolbar-btn icon-btn"
22+
className={buttonClassName}
2323
aria-label={fullLabel}
2424
disableRipple
2525
ref={anchorRef}
26-
{...otherProps}
26+
{...propsWithoutActive}
2727
>
2828
<span className="ct-epb toolbar-btn-inner" tabIndex="-1">
2929
<span className="material-icons">{icon}</span>
@@ -32,12 +32,12 @@ function ToolButton({
3232
) : (
3333
<Button
3434
onClick={onClick}
35-
className="ct-epb toolbar-btn"
35+
className={buttonClassNameText}
3636
aria-label={fullLabel}
3737
disableRipple
3838
endIcon={<span className="material-icons">arrow_drop_down</span>}
3939
ref={anchorRef}
40-
{...otherProps}
40+
{...propsWithoutActive}
4141
>
4242
<span className="ct-epb toolbar-btn-inner" tabIndex="-1">
4343
{label}

src/screens/EPub/controllers/file-builders/LatexFileBuilder.js

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,30 +67,49 @@ class LatexFileBuilder {
6767
}
6868

6969
saveImage(content) {
70-
const img_path = `images/${content.id}.jpeg`
71-
this.zip.addFile(img_path, content.buffer);
72-
return img_path
70+
if (!content || !content.id) {
71+
throw new Error('Invalid image content: missing id');
72+
}
73+
if (!content.buffer || content.buffer.length === 0) {
74+
throw new Error(`Invalid image content: missing or empty buffer for image ${content.id}`);
75+
}
76+
const img_path = `images/${content.id}.jpeg`;
77+
// Ensure images directory exists in zip (AdmZip handles this automatically)
78+
this.zip.addFile(img_path, Buffer.from(content.buffer));
79+
return img_path;
7380
}
7481
convertContent(content) {
7582
if (epubIsText(content)) {
7683
return LatexFileBuilder.markdownToLatex(content);
7784
}
78-
const img_path = this.saveImage(content);
79-
const captions = _.map(content.descriptions, (d) => {
80-
const new_desc = LatexFileBuilder.markdownToLatex(d);
81-
return `\\caption*{${new_desc}}`
82-
}).join("\n");
83-
84-
const new_alt = LatexFileBuilder.escapeSpecialChars(content.alt);
85-
return [
86-
`\\begin{figure}`,
87-
`\\centering`,
88-
this.videoLinks && content.link && content.link !== "" ? `\\href{${content.link}}{` : "",
89-
`\\includegraphics[alt={${new_alt}}, width=.8\\textwidth]{${img_path}}`,
90-
this.videoLinks && content.link && content.link !== "" ? `}` : "",
91-
captions,
92-
`\\end{figure}`
93-
].join("\n")
85+
86+
// Validate image content before processing
87+
if (!content || !content.buffer || content.buffer.length === 0) {
88+
console.warn('Skipping image with missing or empty buffer:', content);
89+
return `% Image skipped: missing or invalid buffer`;
90+
}
91+
92+
try {
93+
const img_path = this.saveImage(content);
94+
const captions = _.map(content.descriptions || [], (d) => {
95+
const new_desc = LatexFileBuilder.markdownToLatex(d);
96+
return `\\caption*{${new_desc}}`
97+
}).join("\n");
98+
99+
const new_alt = LatexFileBuilder.escapeSpecialChars(content.alt || '');
100+
return [
101+
`\\begin{figure}`,
102+
`\\centering`,
103+
this.videoLinks && content.link && content.link !== "" ? `\\href{${content.link}}{` : "",
104+
`\\includegraphics[alt={${new_alt}}, width=.8\\textwidth]{${img_path}}`,
105+
this.videoLinks && content.link && content.link !== "" ? `}` : "",
106+
captions,
107+
`\\end{figure}`
108+
].join("\n");
109+
} catch (error) {
110+
console.error('Error converting image to LaTeX:', error, content);
111+
return `% Image conversion error: ${error.message}`;
112+
}
94113
}
95114

96115
convertChapter(idx, chapter) {
@@ -132,10 +151,12 @@ class LatexFileBuilder {
132151
const glossary = this.data.chapterGlossary ? "" : this.convertGlossary(this.glossary)
133152
return [
134153
"\\documentclass{article}",
154+
"\\usepackage[utf8]{inputenc}",
155+
"\\usepackage[T1]{fontenc}",
135156
"\\usepackage{caption}",
136157
"\\usepackage{graphicx}",
137158
"\\usepackage{hyperref}",
138-
"\\usepackage[T1]{fontenc}",
159+
"\\usepackage{textcomp}",
139160
"\\begin{document}",
140161
titlepage,
141162
TOC,
@@ -227,6 +248,9 @@ class LatexFileBuilder {
227248
}
228249

229250
static escapeSpecialChars(str) {
251+
if (!str) return str;
252+
253+
// First escape ASCII special characters
230254
str = str.replace(/\\/g, '\\textbackslash ');
231255
str = str.replace(/\$/g, '\\$');
232256
str = str.replace(/\{/g, '\\{');
@@ -237,6 +261,17 @@ class LatexFileBuilder {
237261
str = str.replace(/_/g, '\\_');
238262
str = str.replace(/%/g, '\\%');
239263
str = str.replace(/~/g, '\\~');
264+
265+
// Handle Unicode characters - convert to LaTeX commands or use proper encoding
266+
// For characters outside ASCII range, we'll use the textcomp package approach
267+
// or convert to LaTeX Unicode commands
268+
str = str.replace(/[\u0080-\uFFFF]/g, (char) => {
269+
// Common Unicode characters that have LaTeX equivalents
270+
// For others, we'll use the character directly with utf8 encoding
271+
// The utf8 inputenc package should handle most Unicode
272+
return char;
273+
});
274+
240275
return str
241276
}
242277
static removeVerbatimEscape(str) {

src/screens/Home/index.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
1-
import React, { useEffect } from 'react';
1+
import React from 'react';
22
import { env } from 'utils';
33
import { CTLayout, CTLoadable, altEl, makeEl } from 'layout';
44
import { ARRAY_INIT } from 'utils/constants';
5-
import { useDispatch, connect } from 'react-redux';
5+
import { connect } from 'react-redux';
66
import { Placeholder, SectionList, CourseFilter, MaintenanceMesg } from './components';
77

8-
98
const HomeWithRedux = (props) => {
109
const { sections, hasDepartmentSections } = props;
1110
// const sections = useSelector((state) => state.home.sections);
1211
// const hasDepartmentSections = useSelector((state) => state.home.hasDepartmentSections);
1312
const layoutProps = CTLayout.createProps({
1413
transition: true,
1514
responsive: true,
16-
footer: true
15+
footer: true,
1716
});
1817
const loading = sections === ARRAY_INIT;
1918
const loaderElement = makeEl(Placeholder);
@@ -23,17 +22,20 @@ const HomeWithRedux = (props) => {
2322

2423
return (
2524
<CTLayout {...layoutProps}>
26-
<h1 className='sr-only'>Course Browser</h1>
25+
<h1 className="sr-only">Course Browser</h1>
2726
<MaintenanceMesg message={maintenance} />
2827
<CTLoadable loading={loading} loadingElement={loaderElement}>
29-
<h2 className='sr-only' role="search">Filter results</h2>{filterElement}
30-
<h2 className='sr-only'>Courses</h2>
28+
<h2 className="sr-only" role="search">
29+
Filter results
30+
</h2>
31+
{filterElement}
32+
<h2 className="sr-only">Courses</h2>
3133
{sectionElement}
3234
</CTLoadable>
3335
</CTLayout>
3436
);
35-
}
37+
};
3638

3739
export const Home = connect(({ home }) => ({
38-
...home
39-
}))(HomeWithRedux);
40+
...home,
41+
}))(HomeWithRedux);

0 commit comments

Comments
 (0)