overleaf-forge 2.7.2 → 2.8.3
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/README.md +23 -2
- package/examples/voice-lint.mjs +51 -0
- package/overleaf-mcp-server.js +89 -3
- package/package.json +3 -2
- package/templates/main.tex +17 -136
package/README.md
CHANGED
|
@@ -10,6 +10,27 @@ A [Model Context Protocol](https://modelcontextprotocol.io) server that lets an
|
|
|
10
10
|
|
|
11
11
|
Editing a LaTeX project through an AI normally means one of two bad options: paste files back and forth by hand, or let the model overwrite whole files and hope it didn't clobber an edit you made in the browser. This server removes both. It treats Overleaf's git remote as the source of truth and gives the model **anchored edits** (replace an exact string, refuse if it moved), a **conflict gate** (a stale or concurrent change is detected, never silently overwritten), and a **build verdict** (compile from scratch, PASS only on zero errors and zero undefined references), so an editing session is safe to run unattended and provable when it's done.
|
|
12
12
|
|
|
13
|
+
## Token economy
|
|
14
|
+
|
|
15
|
+
Safety is one motivation; keeping a large document out of the model's context window is the other, and it drove most of the tool design. A 50 KB chapter is about 12K to 13K tokens, so the cost of a naive workflow is dominated by moving that whole file in and out.
|
|
16
|
+
|
|
17
|
+
Rough token cost per operation on such a chapter, and the cut each tool buys:
|
|
18
|
+
|
|
19
|
+
| Operation (on a ~50 KB chapter) | Whole-file workflow | overleaf-forge | Reduction |
|
|
20
|
+
| --- | --- | --- | --- |
|
|
21
|
+
| One surgical edit | ~25K (read + write the file) | ~0.15K (`edit_file`) | ~99% |
|
|
22
|
+
| A dozen edits (one revision pass) | ~170K | ~3K | ~98% |
|
|
23
|
+
| One compile check | ~1.5K (raw `latexmk` log) | ~0.02K (`verify_build` verdict) | ~99% |
|
|
24
|
+
| Locating a passage | ~13K (read the whole file) | ~2K (`get_section_content` / `search_text`) | ~85% |
|
|
25
|
+
|
|
26
|
+
Figures are order-of-magnitude, for a chapter this size; the absolute numbers scale with file size, the percentages roughly hold.
|
|
27
|
+
|
|
28
|
+
- **Anchored edits instead of whole-file rewrites.** Changing one phrase by reading the whole file and writing it back costs roughly 25K tokens per edit: the file into context, then the file back out as the write payload. `edit_file` sends only the old and new strings and returns a one-line confirmation, on the order of 100 tokens. Across a dozen edits to a single chapter that is the difference between roughly 170K tokens and 3K.
|
|
29
|
+
- **A one-line build verdict instead of a raw log.** `verify_build` returns `✓ PASS — 24 pages` rather than the `latexmk` output. A raw log runs to hundreds or thousands of tokens per compile, and a multi-pass log buries the true final state under transient undefined-reference warnings from early passes (the exact trap `verify_build` classifies away by reading the final log). Over a session of repeated compiles that is a few thousand tokens against a few dozen.
|
|
30
|
+
- **Section and grep reads instead of the whole file.** `get_section_content` returns one section and `search_text` returns the matching lines, so locating something costs 1K to 3K tokens rather than the full 13K.
|
|
31
|
+
|
|
32
|
+
For an iterative edit, build, and review loop on a large document the tool traffic runs about an order of magnitude lighter than a read-and-rewrite-the-whole-file approach. The gain is workflow-dependent: a single full-file rewrite is a wash, since `write_file` moves the same bytes either way. It is the repeated, surgical work that compounds, which is exactly the shape of writing and revising a paper.
|
|
33
|
+
|
|
13
34
|
## Features
|
|
14
35
|
|
|
15
36
|
- **Surgical, conflict-safe edits**: `edit_file` replaces an exact anchor and refuses if the region changed on Overleaf; non-overlapping concurrent edits auto-merge via git.
|
|
@@ -150,7 +171,7 @@ That creates `~/.overleaf-mcp/projects.json` from the example and copies the edi
|
|
|
150
171
|
| `settings.gitToken` | Overleaf git token, used by every project (or set `OVERLEAF_GIT_TOKEN`). A per-project `gitToken` overrides it. |
|
|
151
172
|
| `settings.repoDir` | Where project clones land by default (`repoDir/<name>`). A per-project `localPath` overrides it. |
|
|
152
173
|
| `settings.templatesDir` | Optional. Directory of scaffold templates, overriding the bundled defaults. |
|
|
153
|
-
| `settings.voiceLinter` | Optional. A prose-linter command (or set `OVERLEAF_VOICE_LINTER`) that `voice_lint` runs on a file.
|
|
174
|
+
| `settings.voiceLinter` | Optional. A prose-linter command (or set `OVERLEAF_VOICE_LINTER`) that `voice_lint` runs on a file. Defaults to the bundled `examples/voice-lint.mjs`; override with your own house style. |
|
|
154
175
|
| `projects.<key>.projectId` | The id from `https://www.overleaf.com/project/<ID>`. |
|
|
155
176
|
| `projects.<key>.cwd` | Directory you launch the client from for this project; used to auto-detect the active project. |
|
|
156
177
|
| `projects.<key>.localPath` | Explicit clone location (optional). |
|
|
@@ -272,7 +293,7 @@ By the built-in convention this parses the name, locates the parent course folde
|
|
|
272
293
|
| `cite_lint` | Report undefined (`\cite` with no entry) and unused (entry never cited) citations. Read-only. |
|
|
273
294
|
| `checkpoint` | Mark a local rollback point (a `mcp-snap/<label>` tag) before a risky edit. |
|
|
274
295
|
| `restore` | Roll back to a checkpoint via a forward commit + push (no force, no history rewrite). |
|
|
275
|
-
| `voice_lint` | Run a
|
|
296
|
+
| `voice_lint` | Run a prose linter on a `.tex` (the bundled `examples/voice-lint.mjs` by default; override via `settings.voiceLinter`). Lints the local working copy as-is, never pulls. Read-only, advisory. |
|
|
276
297
|
|
|
277
298
|
## How it works
|
|
278
299
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Example prose linter for overleaf-forge's `voice_lint` tool.
|
|
3
|
+
//
|
|
4
|
+
// Contract: take a file path as the last argument, print findings to stderr,
|
|
5
|
+
// exit 2 if anything was found and 0 if clean. `voice_lint` runs whatever
|
|
6
|
+
// command settings.voiceLinter (or $OVERLEAF_VOICE_LINTER) points at; this
|
|
7
|
+
// script is the bundled default so the tool works with no configuration.
|
|
8
|
+
//
|
|
9
|
+
// The checks are deliberately generic writing hygiene, chosen to rarely
|
|
10
|
+
// false-positive on LaTeX. Replace them with your own house style: copy this
|
|
11
|
+
// file, edit CHECKS, and point settings.voiceLinter at your copy
|
|
12
|
+
// (e.g. "node /path/to/your-linter.mjs").
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
|
|
15
|
+
const file = process.argv[2];
|
|
16
|
+
if (!file) process.exit(0);
|
|
17
|
+
let text;
|
|
18
|
+
try { text = readFileSync(file, 'utf8'); } catch { process.exit(0); }
|
|
19
|
+
|
|
20
|
+
// [regex, label]. Each is a documented, broadly-defensible prose check.
|
|
21
|
+
const CHECKS = [
|
|
22
|
+
[/\b(TODO|FIXME|XXX)\b/, 'leftover marker (TODO/FIXME/XXX)'],
|
|
23
|
+
[/\b(\w+)\s+\1\b/i, 'repeated word'],
|
|
24
|
+
[/\bin order to\b/i, "wordy: 'in order to' -> 'to'"],
|
|
25
|
+
[/\b(very|really|quite|basically|actually) \w/i, 'filler intensifier (often cuttable)'],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Skip code/verbatim environments and comment lines so source does not false-positive.
|
|
29
|
+
const SKIP_BEGIN = /\\begin\{(lstlisting|verbatim|minted|Verbatim)\}/;
|
|
30
|
+
const SKIP_END = /\\end\{(lstlisting|verbatim|minted|Verbatim)\}/;
|
|
31
|
+
|
|
32
|
+
const findings = [];
|
|
33
|
+
let skipping = false;
|
|
34
|
+
text.split(/\r?\n/).forEach((line, i) => {
|
|
35
|
+
if (SKIP_BEGIN.test(line)) { skipping = true; return; }
|
|
36
|
+
if (SKIP_END.test(line)) { skipping = false; return; }
|
|
37
|
+
if (skipping || line.trimStart().startsWith('%')) return;
|
|
38
|
+
for (const [rx, label] of CHECKS) {
|
|
39
|
+
if (rx.test(line)) findings.push(`${i + 1}: ${label}\n > ${line.trim().slice(0, 90)}`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (findings.length) {
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
`voice-lint (bundled example): ${findings.length} finding(s)\n` +
|
|
46
|
+
findings.map(f => ' ' + f).join('\n') + '\n' +
|
|
47
|
+
'(this is the generic example linter; set settings.voiceLinter for your own rules)\n',
|
|
48
|
+
);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
process.exit(0);
|
package/overleaf-mcp-server.js
CHANGED
|
@@ -189,6 +189,35 @@ function resolveLocalPath(config, projectKey, project) {
|
|
|
189
189
|
return path.join(DEFAULT_REPO_DIR, project.name || projectKey);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// The global settings the `configure` tool manages, each with the question to
|
|
193
|
+
// ask the user when it is unset. Order is the order to ask in.
|
|
194
|
+
const SETTING_HELP = {
|
|
195
|
+
repoDir: 'Where should project clones land by default? (absolute path; clones go to repoDir/<name>)',
|
|
196
|
+
academicRoot: 'Where do your course folders live? (bootstrap_ssa searches academicRoot/Year <n>/Q*/<COURSE>*)',
|
|
197
|
+
ssaSubdir: 'What subfolder name should new SSAs go under inside each course folder? (e.g. "MY SSAs")',
|
|
198
|
+
templatesDir: 'Use your own scaffold templates? Give the directory holding main.tex / context-scaffold.md (blank keeps the bundled examples).',
|
|
199
|
+
voiceLinter: 'Use your own prose linter for voice_lint? Give the command (takes a file path, exits non-zero on findings; blank keeps the bundled example).',
|
|
200
|
+
gitToken: 'Overleaf git token (prefer the OVERLEAF_GIT_TOKEN env var; set here only if you must store it in projects.json).',
|
|
201
|
+
};
|
|
202
|
+
const SETTING_KEYS = Object.keys(SETTING_HELP);
|
|
203
|
+
const SETTING_PATHY = new Set(['repoDir', 'academicRoot', 'templatesDir']);
|
|
204
|
+
|
|
205
|
+
// Merge the provided settings into the current ones, expanding a leading ~ for
|
|
206
|
+
// path-like fields. Pure (homeDir injected) so it is unit-testable. Returns the
|
|
207
|
+
// new settings object and the list of keys actually changed.
|
|
208
|
+
export function mergeSettings(current, args, homeDir) {
|
|
209
|
+
const settings = { ...(current || {}) };
|
|
210
|
+
const provided = [];
|
|
211
|
+
for (const k of SETTING_KEYS) {
|
|
212
|
+
if (args[k] === undefined) continue;
|
|
213
|
+
let v = args[k];
|
|
214
|
+
if (typeof v === 'string' && SETTING_PATHY.has(k) && v.startsWith('~')) v = path.join(homeDir, v.slice(1));
|
|
215
|
+
settings[k] = v;
|
|
216
|
+
provided.push(k);
|
|
217
|
+
}
|
|
218
|
+
return { settings, provided };
|
|
219
|
+
}
|
|
220
|
+
|
|
192
221
|
// Default project resolution:
|
|
193
222
|
// 1. explicit projectName argument
|
|
194
223
|
// 2. project whose `cwd` is a prefix of SESSION_CWD (longest match wins)
|
|
@@ -534,8 +563,13 @@ class OverleafGitClient {
|
|
|
534
563
|
if (!command || !command.trim()) {
|
|
535
564
|
throw new Error('No voice linter configured. Set settings.voiceLinter in projects.json or the OVERLEAF_VOICE_LINTER env var (e.g. a command that takes a file path and exits non-zero on findings).');
|
|
536
565
|
}
|
|
537
|
-
|
|
566
|
+
// Lint the local working copy as-is: never pulls, no network. Advisory and
|
|
567
|
+
// read-only, so it reflects on-disk state including edits not yet pushed. If
|
|
568
|
+
// the clone is absent, say so rather than silently fetching.
|
|
538
569
|
const abs = path.join(this.repoPath, filePath);
|
|
570
|
+
if (!existsSync(abs)) {
|
|
571
|
+
throw new Error(`voice_lint: ${filePath} not found in the local clone (${this.repoPath}). voice_lint reads the local working copy and never pulls; open the project once (list_files / read_file) to clone it, then lint.`);
|
|
572
|
+
}
|
|
539
573
|
const home = os.homedir();
|
|
540
574
|
const parts = command.trim().split(/\s+/).map(t => (t.startsWith('~') ? home + t.slice(1) : t));
|
|
541
575
|
const [prog, ...rest] = parts;
|
|
@@ -983,6 +1017,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
983
1017
|
},
|
|
984
1018
|
},
|
|
985
1019
|
},
|
|
1020
|
+
{
|
|
1021
|
+
name: 'configure',
|
|
1022
|
+
description: 'Set up or update overleaf-forge\'s global settings: the scaffold templates directory, the voice_lint command, and the recurring-work settings bootstrap_ssa uses (academic root, SSA subdir, default clone dir). FIRST call this with NO arguments to see the current settings and which are unset, with a question for each; ask the user those questions in turn; THEN call it again with their answers to write them to projects.json. Omit a field to leave it unchanged; pass an empty string to clear it back to the bundled default. The git token is redacted in all output. Intended right after install to configure the server conversationally.',
|
|
1023
|
+
inputSchema: {
|
|
1024
|
+
type: 'object',
|
|
1025
|
+
properties: {
|
|
1026
|
+
repoDir: { type: 'string', description: 'Default clone location; clones go to repoDir/<name>. Absolute path (a leading ~ is expanded).' },
|
|
1027
|
+
academicRoot: { type: 'string', description: 'Root folder bootstrap_ssa searches: academicRoot/Year <n>/Q*/<COURSE>*. Absolute path (~ expanded).' },
|
|
1028
|
+
ssaSubdir: { type: 'string', description: 'Subfolder name created under each course folder for new SSAs, e.g. "MY SSAs".' },
|
|
1029
|
+
templatesDir: { type: 'string', description: 'Directory of your scaffold templates (main.tex, context-scaffold.md), overriding the bundled examples. ~ expanded. Empty string reverts to bundled.' },
|
|
1030
|
+
voiceLinter: { type: 'string', description: 'Prose-linter command for voice_lint (takes a file path, exits non-zero on findings), overriding the bundled example. Empty string reverts to bundled.' },
|
|
1031
|
+
gitToken: { type: 'string', description: 'Overleaf git token. Prefer the OVERLEAF_GIT_TOKEN env var; set here only to store it in projects.json.' },
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
986
1035
|
{
|
|
987
1036
|
name: 'register_project',
|
|
988
1037
|
description: 'Add or overwrite a project entry in projects.json. Lets you onboard a new Overleaf project without hand-editing JSON. Uses the global gitToken from settings. After registering, also call update_context to set the project notes.',
|
|
@@ -1194,7 +1243,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
1194
1243
|
},
|
|
1195
1244
|
{
|
|
1196
1245
|
name: 'voice_lint',
|
|
1197
|
-
description: 'Lint a .tex file
|
|
1246
|
+
description: 'Lint a .tex file for prose issues. Runs a bundled generic example linter by default; override with settings.voiceLinter in projects.json or the OVERLEAF_VOICE_LINTER env var (a command that takes a file path and exits non-zero on findings). Lints the LOCAL working copy as-is and never pulls, so it reflects on-disk state including edits not yet pushed; if the project has not been cloned locally yet it errors rather than fetching. Read-only and advisory: reports output, never blocks. Useful after editing prose via edit_file/write_file, which bypass any local editor hooks.',
|
|
1198
1247
|
inputSchema: {
|
|
1199
1248
|
type: 'object',
|
|
1200
1249
|
properties: { filePath: { type: 'string' }, projectName: { type: 'string' } },
|
|
@@ -1366,6 +1415,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1366
1415
|
return { content: [{ type: 'text', text: out.join('\n') }] };
|
|
1367
1416
|
}
|
|
1368
1417
|
|
|
1418
|
+
case 'configure': {
|
|
1419
|
+
const config = await loadConfig();
|
|
1420
|
+
config.settings = config.settings || {};
|
|
1421
|
+
const redact = (s) => { const r = { ...s }; if (r.gitToken) r.gitToken = '(set)'; return r; };
|
|
1422
|
+
const { settings, provided } = mergeSettings(config.settings, args, os.homedir());
|
|
1423
|
+
|
|
1424
|
+
if (provided.length === 0) {
|
|
1425
|
+
// Report mode: show current state and the questions for what is unset.
|
|
1426
|
+
const unset = SETTING_KEYS.filter(k => config.settings[k] === undefined || config.settings[k] === '');
|
|
1427
|
+
const lines = [
|
|
1428
|
+
'# overleaf-forge configuration',
|
|
1429
|
+
'',
|
|
1430
|
+
'Current settings:',
|
|
1431
|
+
'```json',
|
|
1432
|
+
JSON.stringify(redact(config.settings), null, 2),
|
|
1433
|
+
'```',
|
|
1434
|
+
'',
|
|
1435
|
+
unset.length
|
|
1436
|
+
? 'Not set yet. Ask the user each of these, then call configure again with their answers (omit any they want to skip):'
|
|
1437
|
+
: 'All settings are set. Pass any field to update it (empty string reverts a template/linter to the bundled default).',
|
|
1438
|
+
...unset.map((k, i) => `${i + 1}. **${k}** — ${SETTING_HELP[k]}`),
|
|
1439
|
+
];
|
|
1440
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Apply mode: persist the provided settings.
|
|
1444
|
+
config.settings = settings;
|
|
1445
|
+
await saveConfig(config);
|
|
1446
|
+
return { content: [{ type: 'text', text: `Updated: ${provided.join(', ')}.\n\`\`\`json\n${JSON.stringify(redact(config.settings), null, 2)}\n\`\`\`\nTakes effect on the next call (projects.json is re-read each time).` }] };
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1369
1449
|
case 'register_project': {
|
|
1370
1450
|
const config = await loadConfig();
|
|
1371
1451
|
if (!args.key || !args.projectId) throw new Error('key and projectId are required');
|
|
@@ -1577,7 +1657,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1577
1657
|
case 'voice_lint': {
|
|
1578
1658
|
const config = await loadConfig();
|
|
1579
1659
|
const { client } = await getClient(args.projectName);
|
|
1580
|
-
|
|
1660
|
+
// settings.voiceLinter / $OVERLEAF_VOICE_LINTER override the bundled
|
|
1661
|
+
// example linter, which ships with the package so voice_lint works out
|
|
1662
|
+
// of the box. The example implements generic prose checks; point the
|
|
1663
|
+
// setting at your own command to enforce a house style.
|
|
1664
|
+
const command = config.settings?.voiceLinter
|
|
1665
|
+
|| process.env.OVERLEAF_VOICE_LINTER
|
|
1666
|
+
|| `node ${path.join(PACKAGE_DIR, 'examples', 'voice-lint.mjs')}`;
|
|
1581
1667
|
const r = await client.voiceLint(args.filePath, { command });
|
|
1582
1668
|
return { content: [{ type: 'text', text: r.clean ? `✓ voice OK — ${args.filePath}${r.findings ? `\n${r.findings}` : ''}` : `voice findings in ${args.filePath}:\n${r.findings}` }] };
|
|
1583
1669
|
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "overleaf-forge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.3",
|
|
4
4
|
"description": "MCP server to read, edit, compile, and verify Overleaf/LaTeX projects over git: conflict-safe edits, figure upload, clean-build verification, citation and voice linting.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "overleaf-mcp-server.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"overleaf-forge": "
|
|
8
|
+
"overleaf-forge": "overleaf-mcp-server.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"overleaf-mcp-server.js",
|
|
12
12
|
"templates/",
|
|
13
|
+
"examples/",
|
|
13
14
|
"writing-guidelines.md",
|
|
14
15
|
"projects.example.json"
|
|
15
16
|
],
|
package/templates/main.tex
CHANGED
|
@@ -1,135 +1,30 @@
|
|
|
1
|
-
%%
|
|
1
|
+
%% Example document template for overleaf-forge's bootstrap_ssa scaffold.
|
|
2
|
+
%%
|
|
3
|
+
%% The tokens __SSA_TITLE__, __SSA_DATE__, and __OVERLEAF_READ_URL__ are
|
|
4
|
+
%% substituted at scaffold time; \author is rewritten when an author is passed.
|
|
5
|
+
%% This is a deliberately plain starting point. Replace it with your own house
|
|
6
|
+
%% style and point settings.templatesDir (or $OVERLEAF_MCP_TEMPLATES) at your copy.
|
|
7
|
+
%% The chapter/appendix/refs layout below matches what reset_ssa_content expects
|
|
8
|
+
%% (Chapters/ch*.tex, Chapters/app_*.tex, refs.bib, figures/).
|
|
2
9
|
\documentclass[a4paper]{article}
|
|
3
10
|
|
|
4
|
-
%% ----- Font -----
|
|
5
11
|
\usepackage[T1]{fontenc}
|
|
6
|
-
\usepackage[
|
|
7
|
-
\renewcommand*\familydefault{\sfdefault}
|
|
8
|
-
|
|
9
|
-
\usepackage[basic,italic,symbolgreek]{mathastext}
|
|
10
|
-
|
|
11
|
-
\makeatletter
|
|
12
|
-
\@for\@tempa:=a,b,c,d,e,h,i,k,l,m,n,o,q,r,s,t,u,v,w,x\do{%
|
|
13
|
-
\MTsetmathskips{\@tempa}{0.5mu}{0.5mu}}%
|
|
14
|
-
\makeatother
|
|
15
|
-
|
|
16
|
-
\MTsetmathskips{f}{2.5mu}{0.5mu}
|
|
17
|
-
\MTsetmathskips{g}{1.5mu}{0.5mu}
|
|
18
|
-
\MTsetmathskips{j}{2.5mu}{0.5mu}
|
|
19
|
-
\MTsetmathskips{p}{1.5mu}{0mu}
|
|
20
|
-
\MTsetmathskips{y}{1.5mu}{0.5mu}
|
|
21
|
-
\MTsetmathskips{z}{1mu}{0.5mu}
|
|
22
|
-
|
|
23
|
-
%% ----- Basics -----
|
|
12
|
+
\usepackage[margin=1in]{geometry}
|
|
24
13
|
\usepackage{graphicx}
|
|
25
14
|
\graphicspath{{figures/}}
|
|
26
15
|
\usepackage{hyperref}
|
|
27
|
-
\usepackage{subcaption}
|
|
28
16
|
\usepackage{xurl}
|
|
29
|
-
\usepackage{
|
|
30
|
-
\usepackage{parskip}
|
|
31
|
-
\usepackage{longtable}
|
|
17
|
+
\usepackage{amsmath}
|
|
32
18
|
\usepackage{siunitx}
|
|
33
|
-
\sisetup{mode=text} %% prevents mathastext/siunitx conflict (\mathrm undefined in XeTeX)
|
|
34
|
-
\usepackage{xcolor}
|
|
35
|
-
\usepackage[margin=1in]{geometry}
|
|
36
|
-
\usepackage{minted}
|
|
37
|
-
\usemintedstyle{fruity}
|
|
38
|
-
\usepackage{listings}
|
|
39
|
-
\usepackage{enumitem}
|
|
40
|
-
\usepackage{wrapfig}
|
|
41
|
-
\usepackage{ragged2e}
|
|
42
|
-
\usepackage{array}
|
|
43
|
-
\usepackage{hhline}
|
|
44
|
-
\usepackage{colortbl}
|
|
45
|
-
\usepackage{marginnote}
|
|
46
|
-
\usepackage{emoji}
|
|
47
|
-
\usepackage{tocloft}
|
|
48
|
-
\usepackage{tcolorbox}
|
|
49
|
-
\tcbuselibrary{skins,breakable}
|
|
50
|
-
\usepackage{titlesec}
|
|
51
|
-
\usepackage{titling}
|
|
52
19
|
\usepackage{booktabs}
|
|
53
|
-
\usepackage{
|
|
54
|
-
\
|
|
55
|
-
\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}}
|
|
56
|
-
\newcolumntype{R}[1]{>{\raggedleft\arraybackslash}p{#1}}
|
|
57
|
-
\usepackage{pdflscape}
|
|
58
|
-
\usepackage{everypage}
|
|
59
|
-
\newcommand{\Lpagenumber}{%
|
|
60
|
-
\ifdim\textwidth=\linewidth\else\bgroup
|
|
61
|
-
\dimendef\margin=0
|
|
62
|
-
\ifodd\value{page}\margin=\oddsidemargin
|
|
63
|
-
\else\margin=\evensidemargin\fi
|
|
64
|
-
\raisebox{\dimexpr-\topmargin-\headheight-\headsep-0.5\linewidth}[0pt][0pt]{%
|
|
65
|
-
\rlap{\hspace{\dimexpr\margin+\textheight+\footskip}%
|
|
66
|
-
\llap{\rotatebox{90}{\thepage}}}}%
|
|
67
|
-
\egroup\fi}
|
|
68
|
-
\AddEverypageHook{\Lpagenumber}
|
|
20
|
+
\usepackage{longtable}
|
|
21
|
+
\usepackage{listings}
|
|
69
22
|
\usepackage[backend=biber,style=ieee,sorting=none]{biblatex}
|
|
70
23
|
\addbibresource{refs.bib}
|
|
71
24
|
|
|
72
|
-
%% ----- Equation annotations -----
|
|
73
|
-
\usepackage{annotate-equations}
|
|
74
|
-
|
|
75
|
-
%% ----- Body text color -----
|
|
76
|
-
\let\originalnormalfont\normalfont
|
|
77
|
-
\renewcommand{\normalfont}{\originalnormalfont\color{black!60}}
|
|
78
|
-
|
|
79
|
-
%% ----- Headings locked to full black -----
|
|
80
|
-
\titleformat{\section}{\normalfont\Large\bfseries\color{black}}{\thesection}{1em}{}
|
|
81
|
-
\titleformat{\subsection}{\normalfont\large\bfseries\color{black}}{\thesubsection}{1em}{}
|
|
82
|
-
\titleformat{\subsubsection}{\normalfont\normalsize\bfseries\color{black}}{\thesubsubsection}{1em}{}
|
|
83
|
-
|
|
84
|
-
%% ----- Title block locked to full black -----
|
|
85
|
-
\pretitle{\begin{center}\color{black}\Large\bfseries}
|
|
86
|
-
\posttitle{\end{center}}
|
|
87
|
-
\preauthor{\begin{center}\color{black}\normalsize}
|
|
88
|
-
\postauthor{\end{center}}
|
|
89
|
-
\predate{\begin{center}\color{black}\normalsize}
|
|
90
|
-
\postdate{\end{center}}
|
|
91
|
-
\titleformat{\paragraph}[hang]{\normalfont\normalsize\bfseries\color{black}}{\theparagraph}{1em}{}
|
|
92
|
-
\titlespacing*{\paragraph}{0pt}{2ex plus 0.5ex minus 0.2ex}{0.5em}
|
|
93
|
-
|
|
94
|
-
%% ----- For Code -----
|
|
95
|
-
\definecolor{codegreen}{rgb}{0,0.6,0}
|
|
96
|
-
\definecolor{codegray}{rgb}{0.5,0.5,0.5}
|
|
97
|
-
\definecolor{codepurple}{rgb}{0.58,0,0.82}
|
|
98
|
-
\definecolor{backcolour}{rgb}{0.95,0.95,0.92}
|
|
99
|
-
\lstdefinestyle{mystyle}{
|
|
100
|
-
backgroundcolor=\color{backcolour},
|
|
101
|
-
commentstyle=\color{codegreen},
|
|
102
|
-
keywordstyle=\color{magenta},
|
|
103
|
-
numberstyle=\tiny\color{codegray},
|
|
104
|
-
stringstyle=\color{codepurple},
|
|
105
|
-
basicstyle=\ttfamily\footnotesize,
|
|
106
|
-
breakatwhitespace=false,
|
|
107
|
-
breaklines=true,
|
|
108
|
-
captionpos=b,
|
|
109
|
-
keepspaces=true,
|
|
110
|
-
numbers=left,
|
|
111
|
-
numbersep=5pt,
|
|
112
|
-
showspaces=false,
|
|
113
|
-
showstringspaces=false,
|
|
114
|
-
showtabs=false,
|
|
115
|
-
tabsize=2
|
|
116
|
-
}
|
|
117
|
-
\lstset{style=mystyle}
|
|
118
|
-
\allowdisplaybreaks
|
|
119
|
-
|
|
120
|
-
%% ----- ToC settings -----
|
|
121
|
-
\renewcommand{\cftsecdotsep}{\cftdotsep}
|
|
122
|
-
\renewcommand{\contentsname}{}
|
|
123
|
-
\renewcommand{\cftdot}{ -. }
|
|
124
|
-
\renewcommand{\cftsecfont}{\color{black}}
|
|
125
|
-
\renewcommand{\cftsubsecfont}{\color{black}}
|
|
126
|
-
\renewcommand{\cftsecpagefont}{\color{black}}
|
|
127
|
-
\renewcommand{\cftsubsecpagefont}{\color{black}}
|
|
128
|
-
|
|
129
|
-
%% ----- Title Page Data -----
|
|
130
25
|
\title{\textbf{__SSA_TITLE__}}
|
|
131
|
-
\author{
|
|
132
|
-
\date{
|
|
26
|
+
\author{Your Name}
|
|
27
|
+
\date{__SSA_DATE__}
|
|
133
28
|
|
|
134
29
|
\begin{document}
|
|
135
30
|
\maketitle
|
|
@@ -137,23 +32,10 @@
|
|
|
137
32
|
\url{__OVERLEAF_READ_URL__}
|
|
138
33
|
\end{center}
|
|
139
34
|
|
|
140
|
-
\
|
|
35
|
+
\tableofcontents
|
|
36
|
+
\clearpage
|
|
141
37
|
|
|
142
|
-
\
|
|
143
|
-
\begin{tcolorbox}[
|
|
144
|
-
arc=0pt,
|
|
145
|
-
colback=white,
|
|
146
|
-
colframe=gray,
|
|
147
|
-
colbacktitle=gray,
|
|
148
|
-
coltitle=white,
|
|
149
|
-
title=DETAILS,
|
|
150
|
-
boxrule=0.5pt,
|
|
151
|
-
fonttitle=\bfseries\large,
|
|
152
|
-
breakable
|
|
153
|
-
]
|
|
154
|
-
\vspace{-3em}
|
|
155
|
-
\tableofcontents
|
|
156
|
-
\end{tcolorbox}
|
|
38
|
+
\input{Chapters/ch1}
|
|
157
39
|
|
|
158
40
|
\clearpage
|
|
159
41
|
\input{Chapters/ch2}
|
|
@@ -165,5 +47,4 @@
|
|
|
165
47
|
\clearpage
|
|
166
48
|
\input{Chapters/app_A}
|
|
167
49
|
|
|
168
|
-
|
|
169
50
|
\end{document}
|