overleaf-comments-export 0.2.0__tar.gz

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.
Files changed (24) hide show
  1. overleaf_comments_export-0.2.0/LICENSE +21 -0
  2. overleaf_comments_export-0.2.0/PKG-INFO +156 -0
  3. overleaf_comments_export-0.2.0/README.md +120 -0
  4. overleaf_comments_export-0.2.0/overleaf_comments_export/__init__.py +1 -0
  5. overleaf_comments_export-0.2.0/overleaf_comments_export/__main__.py +143 -0
  6. overleaf_comments_export-0.2.0/overleaf_comments_export/anchors.py +61 -0
  7. overleaf_comments_export-0.2.0/overleaf_comments_export/client.py +440 -0
  8. overleaf_comments_export-0.2.0/overleaf_comments_export/export.py +892 -0
  9. overleaf_comments_export-0.2.0/overleaf_comments_export/gui.py +583 -0
  10. overleaf_comments_export-0.2.0/overleaf_comments_export/model.py +92 -0
  11. overleaf_comments_export-0.2.0/overleaf_comments_export/render.py +370 -0
  12. overleaf_comments_export-0.2.0/overleaf_comments_export/sections.py +96 -0
  13. overleaf_comments_export-0.2.0/overleaf_comments_export.egg-info/PKG-INFO +156 -0
  14. overleaf_comments_export-0.2.0/overleaf_comments_export.egg-info/SOURCES.txt +22 -0
  15. overleaf_comments_export-0.2.0/overleaf_comments_export.egg-info/dependency_links.txt +1 -0
  16. overleaf_comments_export-0.2.0/overleaf_comments_export.egg-info/entry_points.txt +2 -0
  17. overleaf_comments_export-0.2.0/overleaf_comments_export.egg-info/requires.txt +10 -0
  18. overleaf_comments_export-0.2.0/overleaf_comments_export.egg-info/top_level.txt +1 -0
  19. overleaf_comments_export-0.2.0/pyproject.toml +69 -0
  20. overleaf_comments_export-0.2.0/setup.cfg +4 -0
  21. overleaf_comments_export-0.2.0/tests/test_anchors_sections.py +106 -0
  22. overleaf_comments_export-0.2.0/tests/test_client.py +123 -0
  23. overleaf_comments_export-0.2.0/tests/test_export.py +127 -0
  24. overleaf_comments_export-0.2.0/tests/test_render.py +171 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shivang (https://github.com/Mangluu)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: overleaf-comments-export
3
+ Version: 0.2.0
4
+ Summary: Export comment threads and tracked changes from an Overleaf project to Markdown + JSON, optimized for AI-agent consumption.
5
+ Author: Shivang
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Mangluu/overleaf-comments-export
8
+ Project-URL: Issues, https://github.com/Mangluu/overleaf-comments-export/issues
9
+ Project-URL: Source, https://github.com/Mangluu/overleaf-comments-export
10
+ Keywords: overleaf,latex,research,comments,review,academic,tracked-changes,ai-agents
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: MacOS
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Text Processing :: Markup :: LaTeX
23
+ Classifier: Topic :: Scientific/Engineering
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: pyoverleaf>=0.1.7
28
+ Requires-Dist: browser-cookie3>=0.19
29
+ Requires-Dist: requests>=2.31
30
+ Requires-Dist: platformdirs>=4.0
31
+ Provides-Extra: gui
32
+ Requires-Dist: sv-ttk>=2.6; extra == "gui"
33
+ Provides-Extra: test
34
+ Requires-Dist: pytest>=8.0; extra == "test"
35
+ Dynamic: license-file
36
+
37
+ # overleaf-comments-export
38
+
39
+ > **⚠️ Unofficial tool.** This is a third-party utility that talks to Overleaf's
40
+ > undocumented internal HTTP endpoints. It is not affiliated with or endorsed
41
+ > by Overleaf. Endpoints may change without notice. Use at your own risk and
42
+ > in accordance with [Overleaf's Terms of Service](https://www.overleaf.com/legal).
43
+
44
+ Export the comment threads and tracked changes from an Overleaf project into
45
+ clean Markdown + structured JSON — designed so an AI assistant (Claude,
46
+ ChatGPT, etc.) can ingest reviewer feedback and help you address it.
47
+
48
+ [![CI](https://github.com/Mangluu/overleaf-comments-export/actions/workflows/ci.yml/badge.svg)](https://github.com/Mangluu/overleaf-comments-export/actions/workflows/ci.yml)
49
+ [![PyPI](https://img.shields.io/pypi/v/overleaf-comments-export.svg)](https://pypi.org/project/overleaf-comments-export/)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
51
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
52
+
53
+ ## Why
54
+
55
+ Overleaf doesn't provide a way to export comments or tracked changes for use
56
+ outside the editor. If you want to:
57
+
58
+ - have an AI agent draft point-by-point replies to reviewers,
59
+ - archive reviewer discussions outside of Overleaf,
60
+ - batch-address feedback across a large paper, or
61
+ - split feedback by reviewer to delegate work,
62
+
63
+ … you currently have to copy comments by hand. This tool automates that, given
64
+ an Overleaf project URL and a logged-in browser session.
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install overleaf-comments-export
70
+ ```
71
+
72
+ Requires Python 3.10+. Works on macOS, Linux, and Windows.
73
+
74
+ ## Quick start
75
+
76
+ ### CLI
77
+
78
+ ```bash
79
+ overleaf-comments-export \
80
+ --project-url https://www.overleaf.com/project/<24-hex-id> \
81
+ --out ./paper-comments \
82
+ --browser safari
83
+ ```
84
+
85
+ The first time you run it, sign in to Overleaf in your browser of choice;
86
+ the tool reads the session cookie from there.
87
+
88
+ ### GUI
89
+
90
+ ```bash
91
+ overleaf-comments-export --gui
92
+ ```
93
+
94
+ Opens a small Tkinter window with all options surfaced. Best for non-technical
95
+ users.
96
+
97
+ ## What it produces
98
+
99
+ In your output folder, by default:
100
+
101
+ | File | Purpose |
102
+ |---|---|
103
+ | `comments-<date>.md` | Human-readable Markdown grouped by file → section → line, with stable IDs (`C001`, `C002`, …) and source-context snippets around each anchor. |
104
+ | `comments.json` | Structured data — `summary`, top-level `threads`, `files`, `comments`, `tracked_changes`, etc. Schema described in `agents.md`. |
105
+ | `comments.jsonl` | One self-contained JSON record per comment for streaming/pipelines. |
106
+ | `agents.md` | A brief instruction file telling an AI agent how to consume the batch. |
107
+ | `by-reviewer/<name>.md` | (Optional, `--per-reviewer`) One Markdown per reviewer with only their threads. |
108
+ | `comments.log` | Diagnostic log for the run. |
109
+
110
+ ## Filtering
111
+
112
+ ```bash
113
+ # Only open comments
114
+ overleaf-comments-export --project-url … --out ./out --no-resolved
115
+
116
+ # Only one reviewer's threads
117
+ overleaf-comments-export --project-url … --out ./out --reviewer "Emma"
118
+
119
+ # Compact (default) vs. detailed (multi-line code-fence) layout
120
+ overleaf-comments-export --project-url … --out ./out --render-mode detailed
121
+
122
+ # Per-reviewer sub-reports under ./out/by-reviewer/
123
+ overleaf-comments-export --project-url … --out ./out --per-reviewer
124
+ ```
125
+
126
+ Full flag reference: `overleaf-comments-export --help`.
127
+
128
+ ## Browser authentication
129
+
130
+ The tool reads the `overleaf_session2` cookie from your browser. Trade-offs
131
+ by browser on macOS:
132
+
133
+ | Browser | Notes |
134
+ |---|---|
135
+ | Safari | Recommended. No Keychain prompt; macOS may ask once for permission to read `~/Library/Cookies/`. |
136
+ | Firefox | No Keychain prompt; plain SQLite cookie store. |
137
+ | Chrome / Edge / Brave | Cookies are AES-encrypted with a Keychain-stored key; **you'll get a Keychain password prompt every run.** Hidden behind an opt-in in the GUI. |
138
+
139
+ On Windows, Chrome 127+ uses App-Bound Encryption that `browser-cookie3`
140
+ doesn't fully decrypt yet — prefer Firefox or Edge on Windows.
141
+
142
+ On Linux, snap-packaged browsers sandbox their cookies — install browsers as
143
+ native packages if you can.
144
+
145
+ ## Status & maintenance
146
+
147
+ This is a personal research utility published in case it's useful to others.
148
+ It is provided as-is, with no guaranteed maintenance, no SLA, and no roadmap.
149
+ Pull requests are welcome; issues may or may not be acted upon.
150
+
151
+ If Overleaf changes their internal API, this tool may stop working until
152
+ someone (you?) adapts it.
153
+
154
+ ## License
155
+
156
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,120 @@
1
+ # overleaf-comments-export
2
+
3
+ > **⚠️ Unofficial tool.** This is a third-party utility that talks to Overleaf's
4
+ > undocumented internal HTTP endpoints. It is not affiliated with or endorsed
5
+ > by Overleaf. Endpoints may change without notice. Use at your own risk and
6
+ > in accordance with [Overleaf's Terms of Service](https://www.overleaf.com/legal).
7
+
8
+ Export the comment threads and tracked changes from an Overleaf project into
9
+ clean Markdown + structured JSON — designed so an AI assistant (Claude,
10
+ ChatGPT, etc.) can ingest reviewer feedback and help you address it.
11
+
12
+ [![CI](https://github.com/Mangluu/overleaf-comments-export/actions/workflows/ci.yml/badge.svg)](https://github.com/Mangluu/overleaf-comments-export/actions/workflows/ci.yml)
13
+ [![PyPI](https://img.shields.io/pypi/v/overleaf-comments-export.svg)](https://pypi.org/project/overleaf-comments-export/)
14
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
15
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/)
16
+
17
+ ## Why
18
+
19
+ Overleaf doesn't provide a way to export comments or tracked changes for use
20
+ outside the editor. If you want to:
21
+
22
+ - have an AI agent draft point-by-point replies to reviewers,
23
+ - archive reviewer discussions outside of Overleaf,
24
+ - batch-address feedback across a large paper, or
25
+ - split feedback by reviewer to delegate work,
26
+
27
+ … you currently have to copy comments by hand. This tool automates that, given
28
+ an Overleaf project URL and a logged-in browser session.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install overleaf-comments-export
34
+ ```
35
+
36
+ Requires Python 3.10+. Works on macOS, Linux, and Windows.
37
+
38
+ ## Quick start
39
+
40
+ ### CLI
41
+
42
+ ```bash
43
+ overleaf-comments-export \
44
+ --project-url https://www.overleaf.com/project/<24-hex-id> \
45
+ --out ./paper-comments \
46
+ --browser safari
47
+ ```
48
+
49
+ The first time you run it, sign in to Overleaf in your browser of choice;
50
+ the tool reads the session cookie from there.
51
+
52
+ ### GUI
53
+
54
+ ```bash
55
+ overleaf-comments-export --gui
56
+ ```
57
+
58
+ Opens a small Tkinter window with all options surfaced. Best for non-technical
59
+ users.
60
+
61
+ ## What it produces
62
+
63
+ In your output folder, by default:
64
+
65
+ | File | Purpose |
66
+ |---|---|
67
+ | `comments-<date>.md` | Human-readable Markdown grouped by file → section → line, with stable IDs (`C001`, `C002`, …) and source-context snippets around each anchor. |
68
+ | `comments.json` | Structured data — `summary`, top-level `threads`, `files`, `comments`, `tracked_changes`, etc. Schema described in `agents.md`. |
69
+ | `comments.jsonl` | One self-contained JSON record per comment for streaming/pipelines. |
70
+ | `agents.md` | A brief instruction file telling an AI agent how to consume the batch. |
71
+ | `by-reviewer/<name>.md` | (Optional, `--per-reviewer`) One Markdown per reviewer with only their threads. |
72
+ | `comments.log` | Diagnostic log for the run. |
73
+
74
+ ## Filtering
75
+
76
+ ```bash
77
+ # Only open comments
78
+ overleaf-comments-export --project-url … --out ./out --no-resolved
79
+
80
+ # Only one reviewer's threads
81
+ overleaf-comments-export --project-url … --out ./out --reviewer "Emma"
82
+
83
+ # Compact (default) vs. detailed (multi-line code-fence) layout
84
+ overleaf-comments-export --project-url … --out ./out --render-mode detailed
85
+
86
+ # Per-reviewer sub-reports under ./out/by-reviewer/
87
+ overleaf-comments-export --project-url … --out ./out --per-reviewer
88
+ ```
89
+
90
+ Full flag reference: `overleaf-comments-export --help`.
91
+
92
+ ## Browser authentication
93
+
94
+ The tool reads the `overleaf_session2` cookie from your browser. Trade-offs
95
+ by browser on macOS:
96
+
97
+ | Browser | Notes |
98
+ |---|---|
99
+ | Safari | Recommended. No Keychain prompt; macOS may ask once for permission to read `~/Library/Cookies/`. |
100
+ | Firefox | No Keychain prompt; plain SQLite cookie store. |
101
+ | Chrome / Edge / Brave | Cookies are AES-encrypted with a Keychain-stored key; **you'll get a Keychain password prompt every run.** Hidden behind an opt-in in the GUI. |
102
+
103
+ On Windows, Chrome 127+ uses App-Bound Encryption that `browser-cookie3`
104
+ doesn't fully decrypt yet — prefer Firefox or Edge on Windows.
105
+
106
+ On Linux, snap-packaged browsers sandbox their cookies — install browsers as
107
+ native packages if you can.
108
+
109
+ ## Status & maintenance
110
+
111
+ This is a personal research utility published in case it's useful to others.
112
+ It is provided as-is, with no guaranteed maintenance, no SLA, and no roadmap.
113
+ Pull requests are welcome; issues may or may not be acted upon.
114
+
115
+ If Overleaf changes their internal API, this tool may stop working until
116
+ someone (you?) adapts it.
117
+
118
+ ## License
119
+
120
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,143 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from .client import OverleafClient
9
+ from .export import ExportResult, run_export
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> int:
13
+ parser = argparse.ArgumentParser(
14
+ prog="overleaf-comments-export",
15
+ description="Export Overleaf comment threads and tracked changes to Markdown.",
16
+ )
17
+ parser.add_argument(
18
+ "--gui",
19
+ action="store_true",
20
+ help="Launch the graphical interface (default if no other args).",
21
+ )
22
+ parser.add_argument(
23
+ "--project-url",
24
+ help="Full Overleaf project URL, e.g. https://www.overleaf.com/project/<24-hex-id>.",
25
+ )
26
+ parser.add_argument(
27
+ "--out",
28
+ type=Path,
29
+ help="Directory to write comments-<date>.md, comments.json, and comments.log into.",
30
+ )
31
+ parser.add_argument(
32
+ "--project-title",
33
+ default=None,
34
+ help="Optional human-readable title for the report header. Defaults to the project id.",
35
+ )
36
+ parser.add_argument(
37
+ "--browser",
38
+ default="auto",
39
+ choices=list(OverleafClient.SUPPORTED_BROWSERS),
40
+ help="Which browser to read cookies from. Default: auto-detect.",
41
+ )
42
+ parser.add_argument(
43
+ "--base-url",
44
+ default="https://www.overleaf.com",
45
+ help="Override the Overleaf base URL (for self-hosted instances).",
46
+ )
47
+ parser.add_argument(
48
+ "-v", "--verbose", action="store_true", help="More logging."
49
+ )
50
+ parser.add_argument(
51
+ "--render-mode",
52
+ choices=["compact", "detailed"],
53
+ default="compact",
54
+ help="Markdown layout: 'compact' (one line per comment, default) or "
55
+ "'detailed' (multi-line code-fence with anchor highlighted).",
56
+ )
57
+ parser.add_argument(
58
+ "--no-open",
59
+ dest="include_open",
60
+ action="store_false",
61
+ help="Skip open (unresolved) comments.",
62
+ )
63
+ parser.add_argument(
64
+ "--no-resolved",
65
+ dest="include_resolved",
66
+ action="store_false",
67
+ help="Skip resolved comments.",
68
+ )
69
+ parser.add_argument(
70
+ "--no-changes",
71
+ dest="include_changes",
72
+ action="store_false",
73
+ help="Skip tracked changes.",
74
+ )
75
+ parser.add_argument(
76
+ "--reviewer",
77
+ action="append",
78
+ default=[],
79
+ metavar="NAME",
80
+ help="Only include threads/changes touching this reviewer "
81
+ "(case-insensitive substring match against name and email). "
82
+ "Pass multiple times for OR-of-reviewers.",
83
+ )
84
+ parser.add_argument(
85
+ "--include-raw",
86
+ action="store_true",
87
+ help="Embed the unprocessed Overleaf API payloads inside comments.json.",
88
+ )
89
+ parser.add_argument(
90
+ "--no-jsonl",
91
+ dest="write_jsonl",
92
+ action="store_false",
93
+ help="Skip writing comments.jsonl (the streaming-friendly companion).",
94
+ )
95
+ parser.add_argument(
96
+ "--per-reviewer",
97
+ action="store_true",
98
+ help="Also write one Markdown per reviewer into by-reviewer/.",
99
+ )
100
+ parser.set_defaults(
101
+ include_open=True,
102
+ include_resolved=True,
103
+ include_changes=True,
104
+ write_jsonl=True,
105
+ )
106
+ args = parser.parse_args(argv)
107
+
108
+ if args.gui or (not args.project_url and not args.out):
109
+ from .gui import launch_gui
110
+ return launch_gui()
111
+
112
+ if not args.project_url or not args.out:
113
+ parser.error("--project-url and --out are required in CLI mode (or pass --gui).")
114
+
115
+ logging.basicConfig(
116
+ level=logging.DEBUG if args.verbose else logging.INFO,
117
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
118
+ stream=sys.stderr,
119
+ )
120
+
121
+ result: ExportResult = run_export(
122
+ project_url=args.project_url,
123
+ out_dir=args.out,
124
+ project_title=args.project_title,
125
+ base_url=args.base_url,
126
+ browser=args.browser,
127
+ verbose=args.verbose,
128
+ include_raw=args.include_raw,
129
+ include_open=args.include_open,
130
+ include_resolved=args.include_resolved,
131
+ include_changes=args.include_changes,
132
+ reviewer_filter=args.reviewer,
133
+ render_mode=args.render_mode,
134
+ write_jsonl=args.write_jsonl,
135
+ per_reviewer_reports=args.per_reviewer,
136
+ progress=lambda msg: print(msg, file=sys.stderr),
137
+ )
138
+ print(f"\nDone. Open: {result.markdown_path}")
139
+ return 0
140
+
141
+
142
+ if __name__ == "__main__":
143
+ sys.exit(main())
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from bisect import bisect_right
4
+
5
+ from .model import DocText
6
+
7
+
8
+ def build_line_starts(text: str) -> list[int]:
9
+ """line_starts[i] = char offset where line (i+1) starts. So
10
+ bisect_right(line_starts, offset) gives the 1-indexed line number."""
11
+ starts = [0]
12
+ for i, ch in enumerate(text):
13
+ if ch == "\n":
14
+ starts.append(i + 1)
15
+ return starts
16
+
17
+
18
+ def offset_to_line_col(line_starts: list[int], offset: int) -> tuple[int, int]:
19
+ """Convert a flat character offset into a 1-indexed (line, column).
20
+ Column is 0-indexed (chars after the line start)."""
21
+ if offset < 0:
22
+ offset = 0
23
+ line_no = bisect_right(line_starts, offset)
24
+ if line_no <= 0:
25
+ line_no = 1
26
+ col = offset - line_starts[line_no - 1]
27
+ return line_no, col
28
+
29
+
30
+ def resolve_anchor(
31
+ doc: DocText, offset: int, anchored_text: str, search_window: int = 200
32
+ ) -> tuple[int, int, int, bool]:
33
+ """Map an offset+expected-text anchor to (resolved_offset, line, col, stale).
34
+
35
+ If text[offset:offset+len(anchored_text)] matches anchored_text, we trust it.
36
+ Otherwise, search +/- search_window characters for the anchored text and
37
+ re-anchor. If still not found, return the original offset's coords and
38
+ mark stale=True."""
39
+ text = doc.text
40
+ n = len(anchored_text)
41
+ if n > 0 and 0 <= offset <= len(text) - n and text[offset : offset + n] == anchored_text:
42
+ line, col = offset_to_line_col(doc.line_starts, offset)
43
+ return offset, line, col, False
44
+
45
+ if n > 0:
46
+ # Nearby search
47
+ lo = max(0, offset - search_window)
48
+ hi = min(len(text), offset + search_window + n)
49
+ idx = text.find(anchored_text, lo, hi)
50
+ if idx != -1:
51
+ line, col = offset_to_line_col(doc.line_starts, idx)
52
+ return idx, line, col, False
53
+ # Last-resort whole-document search; mark stale (since it moved far)
54
+ # but still produce a usable line/col rather than guessing.
55
+ idx = text.find(anchored_text)
56
+ if idx != -1:
57
+ line, col = offset_to_line_col(doc.line_starts, idx)
58
+ return idx, line, col, True
59
+
60
+ line, col = offset_to_line_col(doc.line_starts, min(offset, max(0, len(text) - 1)))
61
+ return offset, line, col, True