@learnpack/learnpack 5.0.335 → 5.0.340
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/run +17 -17
- package/lib/commands/init.js +41 -41
- package/lib/commands/serve.js +589 -126
- package/lib/creatorDist/assets/index-BhqDgBS9.js +8448 -78631
- package/lib/creatorDist/assets/index-CjddKHB_.css +1 -1688
- package/lib/managers/config/exercise.js +2 -14
- package/lib/managers/readmeHistoryService.js +3 -1
- package/lib/managers/server/routes.js +2 -1
- package/lib/utils/configBuilder.js +2 -1
- package/lib/utils/creatorUtilities.js +14 -14
- package/lib/utils/exerciseFileOrder.d.ts +20 -0
- package/lib/utils/exerciseFileOrder.js +49 -0
- package/lib/utils/export/epub.js +26 -26
- package/lib/utils/readmeSanitizer.d.ts +8 -0
- package/lib/utils/readmeSanitizer.js +13 -0
- package/lib/utils/templates/epub/epub.css +146 -146
- package/lib/utils/templates/scorm/config/api.js +175 -175
- package/package.json +1 -1
- package/src/commands/init.ts +655 -655
- package/src/commands/publish.ts +670 -670
- package/src/commands/serve.ts +5853 -5216
- package/src/creator/eslint.config.js +28 -28
- package/src/creator/src/index.css +227 -227
- package/src/creator/src/utils/lib.ts +471 -471
- package/src/creatorDist/assets/index-BhqDgBS9.js +8448 -78631
- package/src/creatorDist/assets/index-CjddKHB_.css +1 -1688
- package/src/managers/config/exercise.ts +3 -15
- package/src/managers/readmeHistoryService.ts +3 -1
- package/src/managers/server/routes.ts +15 -6
- package/src/managers/session.ts +184 -184
- package/src/ui/_app/app.css +1 -1
- package/src/ui/_app/app.js +1950 -1878
- package/src/ui/app.tar.gz +0 -0
- package/src/utils/api.ts +675 -675
- package/src/utils/configBuilder.ts +102 -100
- package/src/utils/creatorUtilities.ts +536 -536
- package/src/utils/errors.ts +108 -108
- package/src/utils/exerciseFileOrder.ts +50 -0
- package/src/utils/export/epub.ts +553 -553
- package/src/utils/export/index.ts +4 -4
- package/src/utils/export/scorm.ts +121 -121
- package/src/utils/export/shared.ts +61 -61
- package/src/utils/export/types.ts +25 -25
- package/src/utils/export/zip.ts +55 -55
- package/src/utils/readmeSanitizer.ts +10 -0
- package/src/utils/rigoActions.ts +642 -642
- package/src/utils/templates/epub/epub.css +146 -146
- package/src/utils/templates/scorm/config/api.js +175 -175
|
@@ -6,6 +6,7 @@ const p = require("path");
|
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const console_1 = require("../../utils/console");
|
|
8
8
|
const allowed_files_1 = require("./allowed_files");
|
|
9
|
+
const exerciseFileOrder_1 = require("../../utils/exerciseFileOrder");
|
|
9
10
|
// function processQuestions(markdown: string): {
|
|
10
11
|
// updatedMarkdown: string
|
|
11
12
|
// questions: { id: string; examples: string[] }[]
|
|
@@ -272,20 +273,7 @@ const filterFiles = (files, basePath = ".") => files
|
|
|
272
273
|
path: basePath + "/" + ex,
|
|
273
274
|
}),
|
|
274
275
|
}))
|
|
275
|
-
.sort((f1, f2) =>
|
|
276
|
-
const score = {
|
|
277
|
-
// sorting priority
|
|
278
|
-
"index.html": 1,
|
|
279
|
-
"styles.css": 2,
|
|
280
|
-
"styles.scss": 2,
|
|
281
|
-
"style.css": 2,
|
|
282
|
-
"style.scss": 2,
|
|
283
|
-
"index.css": 2,
|
|
284
|
-
"index.scss": 2,
|
|
285
|
-
"index.js": 3,
|
|
286
|
-
};
|
|
287
|
-
return score[f1.name] < score[f2.name] ? -1 : 1;
|
|
288
|
-
});
|
|
276
|
+
.sort((f1, f2) => (0, exerciseFileOrder_1.compareExerciseFileNames)(f1.name, f2.name));
|
|
289
277
|
exports.filterFiles = filterFiles;
|
|
290
278
|
exports.default = {
|
|
291
279
|
exercise: exports.exercise,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ReadmeHistoryService = void 0;
|
|
4
|
+
const readmeSanitizer_1 = require("../utils/readmeSanitizer");
|
|
4
5
|
/**
|
|
5
6
|
* Service to handle README file updates with automatic history tracking
|
|
6
7
|
* Encapsulates the logic of saving state to history before updating files
|
|
@@ -169,9 +170,10 @@ class ReadmeHistoryService {
|
|
|
169
170
|
* @returns void
|
|
170
171
|
*/
|
|
171
172
|
async saveToGCS(courseSlug, exerciseSlug, fileName, content) {
|
|
173
|
+
const sanitizedContent = (0, readmeSanitizer_1.sanitizeReadmeNewlines)(content);
|
|
172
174
|
const filePath = `courses/${courseSlug}/exercises/${exerciseSlug}/${fileName}`;
|
|
173
175
|
const file = this.bucket.file(filePath);
|
|
174
|
-
await file.save(
|
|
176
|
+
await file.save(sanitizedContent, {
|
|
175
177
|
resumable: false,
|
|
176
178
|
});
|
|
177
179
|
}
|
|
@@ -13,6 +13,7 @@ const session_1 = require("../../managers/session");
|
|
|
13
13
|
const telemetry_1 = require("../telemetry");
|
|
14
14
|
const creatorUtilities_1 = require("../../utils/creatorUtilities");
|
|
15
15
|
const rigoActions_1 = require("../../utils/rigoActions");
|
|
16
|
+
const readmeSanitizer_1 = require("../../utils/readmeSanitizer");
|
|
16
17
|
const sidebarGenerator_1 = require("../../utils/sidebarGenerator");
|
|
17
18
|
const path = require("path");
|
|
18
19
|
// import { eventManager } from "../../utils/osOperations"
|
|
@@ -303,7 +304,7 @@ async function default_1(app, configObject, configManager) {
|
|
|
303
304
|
const readme = exercise.getReadme(null);
|
|
304
305
|
await Promise.all(languagesToTranslate.map(async (language) => {
|
|
305
306
|
const response = await (0, rigoActions_1.translateExercise)(rigoToken, {
|
|
306
|
-
text_to_translate: readme.body,
|
|
307
|
+
text_to_translate: (0, readmeSanitizer_1.sanitizeReadmeNewlines)(readme.body),
|
|
307
308
|
output_language: language,
|
|
308
309
|
}, `${process.env.HOST}/webhooks/translate-exercise`);
|
|
309
310
|
await (0, creatorUtilities_1.saveTranslatedReadme)(slug, response.parsed.output_language_code, response.parsed.translation);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildConfig = buildConfig;
|
|
4
|
+
const exerciseFileOrder_1 = require("./exerciseFileOrder");
|
|
4
5
|
function naturalCompare(a, b) {
|
|
5
6
|
// Split by dots and hyphens, compare numbers as numbers
|
|
6
7
|
const regex = /(\d+|\D+)/g;
|
|
@@ -64,7 +65,7 @@ async function buildConfig(bucket, courseSlug) {
|
|
|
64
65
|
}
|
|
65
66
|
const exercises = Object.values(map)
|
|
66
67
|
.sort((a, b) => naturalCompare(a.slug, b.slug))
|
|
67
|
-
.map((ex, i) => (Object.assign(Object.assign({}, ex), { position: i })));
|
|
68
|
+
.map((ex, i) => (Object.assign(Object.assign({}, ex), { files: (0, exerciseFileOrder_1.sortExerciseFiles)(ex.files), position: i })));
|
|
68
69
|
return {
|
|
69
70
|
config: Object.assign({}, learnJson),
|
|
70
71
|
exercises,
|
|
@@ -334,13 +334,13 @@ function countSentences(text) {
|
|
|
334
334
|
function howManyDifficultParagraphs(paragraphs, maxFKGL) {
|
|
335
335
|
return paragraphs.filter(paragraph => paragraph.fkgl > maxFKGL).length;
|
|
336
336
|
}
|
|
337
|
-
const example_content = `Write or paste your table of content below this line, each topic should be defined on a new line, here is an example:
|
|
338
|
-
|
|
339
|
-
Introduction to AI: Explain what is AI and its applications
|
|
340
|
-
Introduction to Machine Learning: Explain what is machine learning and its applications
|
|
341
|
-
What is an AI Model: Explain what is an AI model and its applications
|
|
342
|
-
How to use an AI Model: Different APIs, local models, etc.
|
|
343
|
-
How to build an AI Model: Fine-tuning, data collection, cleaning and more.
|
|
337
|
+
const example_content = `Write or paste your table of content below this line, each topic should be defined on a new line, here is an example:
|
|
338
|
+
|
|
339
|
+
Introduction to AI: Explain what is AI and its applications
|
|
340
|
+
Introduction to Machine Learning: Explain what is machine learning and its applications
|
|
341
|
+
What is an AI Model: Explain what is an AI model and its applications
|
|
342
|
+
How to use an AI Model: Different APIs, local models, etc.
|
|
343
|
+
How to build an AI Model: Fine-tuning, data collection, cleaning and more.
|
|
344
344
|
`;
|
|
345
345
|
const appendContentIndex = async () => {
|
|
346
346
|
const choices = await prompts([
|
|
@@ -370,13 +370,13 @@ const appendContentIndex = async () => {
|
|
|
370
370
|
return null;
|
|
371
371
|
};
|
|
372
372
|
exports.appendContentIndex = appendContentIndex;
|
|
373
|
-
const example_airules = `
|
|
374
|
-
Write with an engaging tone, use simple words and avoid complex sentences.
|
|
375
|
-
Write in first person, as if you are talking to the reader.
|
|
376
|
-
Add mental maps to help the reader understand the content.
|
|
377
|
-
Add diagrams to help the reader understand the content.
|
|
378
|
-
No code exercises required
|
|
379
|
-
|
|
373
|
+
const example_airules = `
|
|
374
|
+
Write with an engaging tone, use simple words and avoid complex sentences.
|
|
375
|
+
Write in first person, as if you are talking to the reader.
|
|
376
|
+
Add mental maps to help the reader understand the content.
|
|
377
|
+
Add diagrams to help the reader understand the content.
|
|
378
|
+
No code exercises required
|
|
379
|
+
|
|
380
380
|
`;
|
|
381
381
|
const appendAIRules = async () => {
|
|
382
382
|
const choices = await prompts([
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ordering logic for exercise file lists (e.g. editor tabs).
|
|
3
|
+
* Ensures solution files (*.solution.hide.*) always appear after their base file.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Comparator for two file names. Use with Array.prototype.sort.
|
|
7
|
+
* Solution files are ordered after their base file; then by priority; then alphabetically.
|
|
8
|
+
* @param nameA - First file name.
|
|
9
|
+
* @param nameB - Second file name.
|
|
10
|
+
* @returns Negative if nameA < nameB, 0 if equal, positive if nameA > nameB.
|
|
11
|
+
*/
|
|
12
|
+
export declare function compareExerciseFileNames(nameA: string, nameB: string): number;
|
|
13
|
+
/**
|
|
14
|
+
* Sorts an array of file-like objects by name using exercise file ordering rules.
|
|
15
|
+
* @param files - Array of objects with a `name` property.
|
|
16
|
+
* @returns A new sorted array (original is not mutated).
|
|
17
|
+
*/
|
|
18
|
+
export declare function sortExerciseFiles<T extends {
|
|
19
|
+
name: string;
|
|
20
|
+
}>(files: T[]): T[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared ordering logic for exercise file lists (e.g. editor tabs).
|
|
4
|
+
* Ensures solution files (*.solution.hide.*) always appear after their base file.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.compareExerciseFileNames = compareExerciseFileNames;
|
|
8
|
+
exports.sortExerciseFiles = sortExerciseFiles;
|
|
9
|
+
const SOLUTION_HIDE = ".solution.hide";
|
|
10
|
+
const isSolutionFile = (name) => name.includes(SOLUTION_HIDE);
|
|
11
|
+
const getBaseFileName = (name) => name.replace(SOLUTION_HIDE, "");
|
|
12
|
+
const FILE_PRIORITY = {
|
|
13
|
+
"index.html": 1,
|
|
14
|
+
"styles.css": 2,
|
|
15
|
+
"styles.scss": 2,
|
|
16
|
+
"style.css": 2,
|
|
17
|
+
"style.scss": 2,
|
|
18
|
+
"index.css": 2,
|
|
19
|
+
"index.scss": 2,
|
|
20
|
+
"index.js": 3,
|
|
21
|
+
};
|
|
22
|
+
const DEFAULT_PRIORITY = Infinity;
|
|
23
|
+
/**
|
|
24
|
+
* Comparator for two file names. Use with Array.prototype.sort.
|
|
25
|
+
* Solution files are ordered after their base file; then by priority; then alphabetically.
|
|
26
|
+
* @param nameA - First file name.
|
|
27
|
+
* @param nameB - Second file name.
|
|
28
|
+
* @returns Negative if nameA < nameB, 0 if equal, positive if nameA > nameB.
|
|
29
|
+
*/
|
|
30
|
+
function compareExerciseFileNames(nameA, nameB) {
|
|
31
|
+
var _a, _b;
|
|
32
|
+
if (isSolutionFile(nameA) && getBaseFileName(nameA) === nameB)
|
|
33
|
+
return 1;
|
|
34
|
+
if (isSolutionFile(nameB) && getBaseFileName(nameB) === nameA)
|
|
35
|
+
return -1;
|
|
36
|
+
const s1 = (_a = FILE_PRIORITY[nameA]) !== null && _a !== void 0 ? _a : DEFAULT_PRIORITY;
|
|
37
|
+
const s2 = (_b = FILE_PRIORITY[nameB]) !== null && _b !== void 0 ? _b : DEFAULT_PRIORITY;
|
|
38
|
+
if (s1 !== s2)
|
|
39
|
+
return s1 - s2;
|
|
40
|
+
return nameA.localeCompare(nameB);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Sorts an array of file-like objects by name using exercise file ordering rules.
|
|
44
|
+
* @param files - Array of objects with a `name` property.
|
|
45
|
+
* @returns A new sorted array (original is not mutated).
|
|
46
|
+
*/
|
|
47
|
+
function sortExerciseFiles(files) {
|
|
48
|
+
return [...files].sort((a, b) => compareExerciseFileNames(a.name, b.name));
|
|
49
|
+
}
|
package/lib/utils/export/epub.js
CHANGED
|
@@ -75,14 +75,14 @@ async function generateEpub(markdownFiles, outputPath, metadata, epubMetadata, c
|
|
|
75
75
|
return "";
|
|
76
76
|
return str.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
77
77
|
};
|
|
78
|
-
const yamlContent = `---
|
|
79
|
-
title: "${epubMetadata.title}"
|
|
80
|
-
creator: "${epubMetadata.creator}"
|
|
81
|
-
publisher: "${epubMetadata.publisher}"
|
|
82
|
-
rights: "${epubMetadata.rights}"
|
|
83
|
-
lang: "${epubMetadata.lang}"
|
|
84
|
-
description: "${escapeYaml(metadata.description || "")}"
|
|
85
|
-
subject: "${((_a = metadata.technologies) === null || _a === void 0 ? void 0 : _a.join(", ")) || "Programming"}"
|
|
78
|
+
const yamlContent = `---
|
|
79
|
+
title: "${epubMetadata.title}"
|
|
80
|
+
creator: "${epubMetadata.creator}"
|
|
81
|
+
publisher: "${epubMetadata.publisher}"
|
|
82
|
+
rights: "${epubMetadata.rights}"
|
|
83
|
+
lang: "${epubMetadata.lang}"
|
|
84
|
+
description: "${escapeYaml(metadata.description || "")}"
|
|
85
|
+
subject: "${((_a = metadata.technologies) === null || _a === void 0 ? void 0 : _a.join(", ")) || "Programming"}"
|
|
86
86
|
---`;
|
|
87
87
|
fs.writeFileSync(yamlPath, yamlContent, "utf-8");
|
|
88
88
|
// Include all markdown files in the EPUB
|
|
@@ -348,20 +348,20 @@ async function exportToEpub(options, epubMetadata) {
|
|
|
348
348
|
const sidebarData = readSidebarData(path.join(epubOutDir, "learn"));
|
|
349
349
|
console.log("📋 Sidebar data loaded:", sidebarData ? "Success" : "Not found");
|
|
350
350
|
// 7. Create main content file as markdown
|
|
351
|
-
const mainContent = `# ${metadata.title}
|
|
352
|
-
|
|
353
|
-
${metadata.description || ""}
|
|
354
|
-
|
|
355
|
-
**Technologies:** ${((_c = metadata.technologies) === null || _c === void 0 ? void 0 : _c.join(", ")) || "Programming"}
|
|
356
|
-
|
|
357
|
-
**Difficulty:** ${metadata.difficulty}
|
|
358
|
-
|
|
359
|
-
**Language:** ${language}
|
|
360
|
-
|
|
361
|
-
---
|
|
362
|
-
|
|
363
|
-
## Table of Contents
|
|
364
|
-
|
|
351
|
+
const mainContent = `# ${metadata.title}
|
|
352
|
+
|
|
353
|
+
${metadata.description || ""}
|
|
354
|
+
|
|
355
|
+
**Technologies:** ${((_c = metadata.technologies) === null || _c === void 0 ? void 0 : _c.join(", ")) || "Programming"}
|
|
356
|
+
|
|
357
|
+
**Difficulty:** ${metadata.difficulty}
|
|
358
|
+
|
|
359
|
+
**Language:** ${language}
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Table of Contents
|
|
364
|
+
|
|
365
365
|
${processedMarkdownFiles
|
|
366
366
|
.map((file, index) => {
|
|
367
367
|
const fileName = path.basename(file, ".processed.md");
|
|
@@ -379,10 +379,10 @@ ${processedMarkdownFiles
|
|
|
379
379
|
}
|
|
380
380
|
return `${index + 1}. ${displayName}`;
|
|
381
381
|
})
|
|
382
|
-
.join("\n")}
|
|
383
|
-
|
|
384
|
-
---
|
|
385
|
-
|
|
382
|
+
.join("\n")}
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
386
|
`;
|
|
387
387
|
const mainContentPath = path.join(epubOutDir, "processed", "main.md");
|
|
388
388
|
mkdirp.sync(path.dirname(mainContentPath));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces sequences of 3+ newlines (with optional spaces in between)
|
|
3
|
+
* with a single double newline. Prevents excessive whitespace that can
|
|
4
|
+
* cause translation API token limit issues.
|
|
5
|
+
* @param content - Raw README string
|
|
6
|
+
* @returns Sanitized string
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeReadmeNewlines(content: string): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeReadmeNewlines = sanitizeReadmeNewlines;
|
|
4
|
+
/**
|
|
5
|
+
* Replaces sequences of 3+ newlines (with optional spaces in between)
|
|
6
|
+
* with a single double newline. Prevents excessive whitespace that can
|
|
7
|
+
* cause translation API token limit issues.
|
|
8
|
+
* @param content - Raw README string
|
|
9
|
+
* @returns Sanitized string
|
|
10
|
+
*/
|
|
11
|
+
function sanitizeReadmeNewlines(content) {
|
|
12
|
+
return content.replace(/(\n\s*){3,}/g, "\n\n");
|
|
13
|
+
}
|
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
/* Basic EPUB styling */
|
|
2
|
-
body {
|
|
3
|
-
font-family: "Georgia", serif;
|
|
4
|
-
line-height: 1.6;
|
|
5
|
-
margin: 2em;
|
|
6
|
-
color: #333;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
h1,
|
|
10
|
-
h2,
|
|
11
|
-
h3,
|
|
12
|
-
h4,
|
|
13
|
-
h5,
|
|
14
|
-
h6 {
|
|
15
|
-
color: #2c3e50;
|
|
16
|
-
margin-top: 1.5em;
|
|
17
|
-
margin-bottom: 0.5em;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
h1 {
|
|
21
|
-
font-size: 2em;
|
|
22
|
-
border-bottom: 2px solid #3498db;
|
|
23
|
-
padding-bottom: 0.3em;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
h2 {
|
|
27
|
-
font-size: 1.5em;
|
|
28
|
-
border-bottom: 1px solid #bdc3c7;
|
|
29
|
-
padding-bottom: 0.2em;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
p {
|
|
33
|
-
margin-bottom: 1em;
|
|
34
|
-
text-align: justify;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
code {
|
|
38
|
-
background-color: #f8f9fa;
|
|
39
|
-
padding: 0.2em 0.4em;
|
|
40
|
-
border-radius: 3px;
|
|
41
|
-
font-family: "Courier New", monospace;
|
|
42
|
-
font-size: 0.9em;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
pre {
|
|
46
|
-
background-color: #f8f9fa;
|
|
47
|
-
padding: 1em;
|
|
48
|
-
border-radius: 5px;
|
|
49
|
-
overflow-x: auto;
|
|
50
|
-
border-left: 4px solid #3498db;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
pre code {
|
|
54
|
-
background-color: transparent;
|
|
55
|
-
padding: 0;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
blockquote {
|
|
59
|
-
border-left: 4px solid #bdc3c7;
|
|
60
|
-
margin: 1em 0;
|
|
61
|
-
padding-left: 1em;
|
|
62
|
-
font-style: italic;
|
|
63
|
-
color: #7f8c8d;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
ul,
|
|
67
|
-
ol {
|
|
68
|
-
margin-bottom: 1em;
|
|
69
|
-
padding-left: 2em;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
li {
|
|
73
|
-
margin-bottom: 0.5em;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
a {
|
|
77
|
-
color: #3498db;
|
|
78
|
-
text-decoration: none;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
a:hover {
|
|
82
|
-
text-decoration: underline;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
img {
|
|
86
|
-
max-width: 100%;
|
|
87
|
-
height: auto;
|
|
88
|
-
display: block;
|
|
89
|
-
margin: 1em auto;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
table {
|
|
93
|
-
border-collapse: collapse;
|
|
94
|
-
width: 100%;
|
|
95
|
-
margin: 1em 0;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
th,
|
|
99
|
-
td {
|
|
100
|
-
border: 1px solid #ddd;
|
|
101
|
-
padding: 8px;
|
|
102
|
-
text-align: left;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
th {
|
|
106
|
-
background-color: #f2f2f2;
|
|
107
|
-
font-weight: bold;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
#toc {
|
|
111
|
-
background-color: #f8f9fa;
|
|
112
|
-
padding: 1em;
|
|
113
|
-
border-radius: 5px;
|
|
114
|
-
margin-bottom: 2em;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
#toc h2 {
|
|
118
|
-
border-bottom: none;
|
|
119
|
-
margin-top: 0;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
#toc ol {
|
|
123
|
-
padding-left: 1em;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
#toc li {
|
|
127
|
-
margin-bottom: 0.3em;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
#toc a {
|
|
131
|
-
color: #2c3e50;
|
|
132
|
-
font-weight: 500;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/* Open question styles */
|
|
136
|
-
.open-question {
|
|
137
|
-
border: 1px solid #c2c2c2;
|
|
138
|
-
padding: 1em;
|
|
139
|
-
background-color: #fafafa;
|
|
140
|
-
margin: 1em 0;
|
|
141
|
-
width: 100%;
|
|
142
|
-
height: 150px;
|
|
143
|
-
color: #c2c2c2;
|
|
144
|
-
border-radius: 5px;
|
|
145
|
-
font-style: italic;
|
|
146
|
-
}
|
|
1
|
+
/* Basic EPUB styling */
|
|
2
|
+
body {
|
|
3
|
+
font-family: "Georgia", serif;
|
|
4
|
+
line-height: 1.6;
|
|
5
|
+
margin: 2em;
|
|
6
|
+
color: #333;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
h1,
|
|
10
|
+
h2,
|
|
11
|
+
h3,
|
|
12
|
+
h4,
|
|
13
|
+
h5,
|
|
14
|
+
h6 {
|
|
15
|
+
color: #2c3e50;
|
|
16
|
+
margin-top: 1.5em;
|
|
17
|
+
margin-bottom: 0.5em;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
h1 {
|
|
21
|
+
font-size: 2em;
|
|
22
|
+
border-bottom: 2px solid #3498db;
|
|
23
|
+
padding-bottom: 0.3em;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
h2 {
|
|
27
|
+
font-size: 1.5em;
|
|
28
|
+
border-bottom: 1px solid #bdc3c7;
|
|
29
|
+
padding-bottom: 0.2em;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
p {
|
|
33
|
+
margin-bottom: 1em;
|
|
34
|
+
text-align: justify;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
code {
|
|
38
|
+
background-color: #f8f9fa;
|
|
39
|
+
padding: 0.2em 0.4em;
|
|
40
|
+
border-radius: 3px;
|
|
41
|
+
font-family: "Courier New", monospace;
|
|
42
|
+
font-size: 0.9em;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pre {
|
|
46
|
+
background-color: #f8f9fa;
|
|
47
|
+
padding: 1em;
|
|
48
|
+
border-radius: 5px;
|
|
49
|
+
overflow-x: auto;
|
|
50
|
+
border-left: 4px solid #3498db;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pre code {
|
|
54
|
+
background-color: transparent;
|
|
55
|
+
padding: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
blockquote {
|
|
59
|
+
border-left: 4px solid #bdc3c7;
|
|
60
|
+
margin: 1em 0;
|
|
61
|
+
padding-left: 1em;
|
|
62
|
+
font-style: italic;
|
|
63
|
+
color: #7f8c8d;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ul,
|
|
67
|
+
ol {
|
|
68
|
+
margin-bottom: 1em;
|
|
69
|
+
padding-left: 2em;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
li {
|
|
73
|
+
margin-bottom: 0.5em;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
a {
|
|
77
|
+
color: #3498db;
|
|
78
|
+
text-decoration: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
a:hover {
|
|
82
|
+
text-decoration: underline;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
img {
|
|
86
|
+
max-width: 100%;
|
|
87
|
+
height: auto;
|
|
88
|
+
display: block;
|
|
89
|
+
margin: 1em auto;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
table {
|
|
93
|
+
border-collapse: collapse;
|
|
94
|
+
width: 100%;
|
|
95
|
+
margin: 1em 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
th,
|
|
99
|
+
td {
|
|
100
|
+
border: 1px solid #ddd;
|
|
101
|
+
padding: 8px;
|
|
102
|
+
text-align: left;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
th {
|
|
106
|
+
background-color: #f2f2f2;
|
|
107
|
+
font-weight: bold;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#toc {
|
|
111
|
+
background-color: #f8f9fa;
|
|
112
|
+
padding: 1em;
|
|
113
|
+
border-radius: 5px;
|
|
114
|
+
margin-bottom: 2em;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#toc h2 {
|
|
118
|
+
border-bottom: none;
|
|
119
|
+
margin-top: 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#toc ol {
|
|
123
|
+
padding-left: 1em;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#toc li {
|
|
127
|
+
margin-bottom: 0.3em;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#toc a {
|
|
131
|
+
color: #2c3e50;
|
|
132
|
+
font-weight: 500;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Open question styles */
|
|
136
|
+
.open-question {
|
|
137
|
+
border: 1px solid #c2c2c2;
|
|
138
|
+
padding: 1em;
|
|
139
|
+
background-color: #fafafa;
|
|
140
|
+
margin: 1em 0;
|
|
141
|
+
width: 100%;
|
|
142
|
+
height: 150px;
|
|
143
|
+
color: #c2c2c2;
|
|
144
|
+
border-radius: 5px;
|
|
145
|
+
font-style: italic;
|
|
146
|
+
}
|