viewers
This commit is contained in:
@@ -11,9 +11,29 @@ from .base import ExtractionResult
|
|||||||
|
|
||||||
MAX_SHEET_ROWS = 200
|
MAX_SHEET_ROWS = 200
|
||||||
|
|
||||||
|
# Generic core-property titles Office writes by default — worse than useless as a
|
||||||
|
# display title (they hide the real one), so we drop them and derive from content.
|
||||||
|
PLACEHOLDER_TITLES = {
|
||||||
|
"word document", "powerpoint presentation", "excel workbook",
|
||||||
|
"microsoft word document", "microsoft powerpoint presentation",
|
||||||
|
"microsoft excel worksheet", "presentation", "workbook", "document",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _good_title(t) -> bool:
|
||||||
|
return bool(t) and str(t).strip().lower() not in PLACEHOLDER_TITLES
|
||||||
|
|
||||||
|
|
||||||
|
def _first_line(text: str, limit: int = 120) -> str:
|
||||||
|
for ln in text.splitlines():
|
||||||
|
s = ln.lstrip("#").strip()
|
||||||
|
if s:
|
||||||
|
return s[:limit]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _core_props(props) -> dict:
|
def _core_props(props) -> dict:
|
||||||
"""Common OOXML core properties → metadata (only non-empty ones).
|
"""Common OOXML core properties → metadata (only non-empty, non-placeholder).
|
||||||
|
|
||||||
python-docx/pptx expose `.author`/`.last_modified_by`; openpyxl uses
|
python-docx/pptx expose `.author`/`.last_modified_by`; openpyxl uses
|
||||||
`.creator`/`.lastModifiedBy`. Each target checks both spellings.
|
`.creator`/`.lastModifiedBy`. Each target checks both spellings.
|
||||||
@@ -31,6 +51,8 @@ def _core_props(props) -> dict:
|
|||||||
for src in sources:
|
for src in sources:
|
||||||
val = getattr(props, src, None)
|
val = getattr(props, src, None)
|
||||||
if val:
|
if val:
|
||||||
|
if dst == "title" and not _good_title(val):
|
||||||
|
continue # skip "Word Document" & friends; derive from content
|
||||||
out[dst] = str(val)
|
out[dst] = str(val)
|
||||||
break
|
break
|
||||||
return out
|
return out
|
||||||
@@ -43,6 +65,10 @@ def _extract_docx(source: Path) -> ExtractionResult:
|
|||||||
content = "\n\n".join(p for p in paras if p.strip()) + "\n"
|
content = "\n\n".join(p for p in paras if p.strip()) + "\n"
|
||||||
metadata = _core_props(doc.core_properties)
|
metadata = _core_props(doc.core_properties)
|
||||||
metadata["paragraphs"] = len([p for p in paras if p.strip()])
|
metadata["paragraphs"] = len([p for p in paras if p.strip()])
|
||||||
|
if "title" not in metadata:
|
||||||
|
t = _first_line(content)
|
||||||
|
if t:
|
||||||
|
metadata["title"] = t
|
||||||
return ExtractionResult(content=content, metadata=metadata, extractor="office/python-docx@1")
|
return ExtractionResult(content=content, metadata=metadata, extractor="office/python-docx@1")
|
||||||
|
|
||||||
|
|
||||||
@@ -62,6 +88,12 @@ def _extract_pptx(source: Path) -> ExtractionResult:
|
|||||||
content = "\n\n".join(blocks) + "\n"
|
content = "\n\n".join(blocks) + "\n"
|
||||||
metadata = _core_props(prs.core_properties)
|
metadata = _core_props(prs.core_properties)
|
||||||
metadata["slides"] = len(prs.slides)
|
metadata["slides"] = len(prs.slides)
|
||||||
|
if "title" not in metadata and prs.slides:
|
||||||
|
# first slide's title placeholder, if any
|
||||||
|
for shape in prs.slides[0].shapes:
|
||||||
|
if shape.has_text_frame and shape.text_frame.text.strip():
|
||||||
|
metadata["title"] = shape.text_frame.text.strip()[:120]
|
||||||
|
break
|
||||||
return ExtractionResult(content=content, metadata=metadata, extractor="office/python-pptx@1")
|
return ExtractionResult(content=content, metadata=metadata, extractor="office/python-pptx@1")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
237
ui/doocus-app/package-lock.json
generated
237
ui/doocus-app/package-lock.json
generated
@@ -9,8 +9,10 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jszip": "^3.10",
|
"jszip": "^3.10",
|
||||||
|
"mammoth": "^1.8",
|
||||||
"marked": "^14",
|
"marked": "^14",
|
||||||
"vue": "^3.5"
|
"vue": "^3.5",
|
||||||
|
"xlsx": "^0.18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
@@ -1060,6 +1062,24 @@
|
|||||||
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
|
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@xmldom/xmldom": {
|
||||||
|
"version": "0.8.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
|
||||||
|
"integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/alien-signals": {
|
"node_modules/alien-signals": {
|
||||||
"version": "1.0.13",
|
"version": "1.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
|
||||||
@@ -1067,6 +1087,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"sprintf-js": "~1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -1074,6 +1103,32 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/bluebird": {
|
||||||
|
"version": "3.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
|
||||||
|
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||||
@@ -1084,12 +1139,46 @@
|
|||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/core-util-is": {
|
"node_modules/core-util-is": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -1103,6 +1192,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dingbat-to-unicode": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/duck": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
|
||||||
|
"license": "BSD",
|
||||||
|
"dependencies": {
|
||||||
|
"underscore": "^1.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
@@ -1181,6 +1285,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
@@ -1245,6 +1358,17 @@
|
|||||||
"immediate": "~3.0.5"
|
"immediate": "~3.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lop": {
|
||||||
|
"version": "0.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
|
||||||
|
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"duck": "^0.1.12",
|
||||||
|
"option": "~0.2.1",
|
||||||
|
"underscore": "^1.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -1254,6 +1378,30 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mammoth": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@xmldom/xmldom": "^0.8.6",
|
||||||
|
"argparse": "~1.0.3",
|
||||||
|
"base64-js": "^1.5.1",
|
||||||
|
"bluebird": "~3.4.0",
|
||||||
|
"dingbat-to-unicode": "^1.0.1",
|
||||||
|
"jszip": "^3.7.1",
|
||||||
|
"lop": "^0.4.2",
|
||||||
|
"path-is-absolute": "^1.0.0",
|
||||||
|
"underscore": "^1.13.1",
|
||||||
|
"xmlbuilder": "^10.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mammoth": "bin/mammoth"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "14.1.4",
|
"version": "14.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz",
|
||||||
@@ -1307,6 +1455,12 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/option": {
|
||||||
|
"version": "0.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
|
||||||
|
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/pako": {
|
"node_modules/pako": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
@@ -1320,6 +1474,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1454,6 +1617,24 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sprintf-js": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
@@ -1494,6 +1675,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/underscore": {
|
||||||
|
"version": "1.13.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
|
||||||
|
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
@@ -1626,6 +1813,54 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlbuilder": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jszip": "^3.10",
|
"jszip": "^3.10",
|
||||||
|
"mammoth": "^1.8",
|
||||||
"marked": "^14",
|
"marked": "^14",
|
||||||
"vue": "^3.5"
|
"vue": "^3.5",
|
||||||
|
"xlsx": "^0.18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
|
|||||||
@@ -2,35 +2,18 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import SplitPane from '@framework/components/SplitPane.vue'
|
import SplitPane from '@framework/components/SplitPane.vue'
|
||||||
|
import NativeViewer from '@/components/NativeViewer.vue'
|
||||||
import { store, humanBytes } from '@/store'
|
import { store, humanBytes } from '@/store'
|
||||||
|
|
||||||
const d = computed(() => store.detail)
|
const d = computed(() => store.detail)
|
||||||
const node = computed(() => d.value?.node)
|
const node = computed(() => d.value?.node)
|
||||||
|
|
||||||
type ViewerKind = 'pdf' | 'image' | 'html' | 'markdown' | 'text' | 'office' | 'video' | 'download'
|
// Left pane (extracted search text) only exists for extracted heavy formats,
|
||||||
|
// and not for pptx (whose viewer already shows the per-slide text).
|
||||||
|
const hasExtracted = computed(() =>
|
||||||
|
node.value?.mode === 'extracted' && node.value.ext !== 'pptx' && d.value?.content != null)
|
||||||
|
|
||||||
const viewerKind = computed<ViewerKind>(() => {
|
const extractedHtml = computed(() => (d.value?.content ? marked.parse(d.value.content) : ''))
|
||||||
const ext = node.value?.ext ?? ''
|
|
||||||
if (ext === 'pdf') return 'pdf'
|
|
||||||
if (['png', 'jpg', 'jpeg'].includes(ext)) return 'image'
|
|
||||||
if (['html', 'htm'].includes(ext)) return 'html'
|
|
||||||
if (['md', 'markdown'].includes(ext)) return 'markdown'
|
|
||||||
if (['txt', 'json', 'yaml', 'yml', 'csv'].includes(ext)) return 'text'
|
|
||||||
if (['docx', 'pptx', 'xlsx'].includes(ext)) return 'office'
|
|
||||||
if (ext === 'mp4') return 'video'
|
|
||||||
return 'download'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Left pane (extracted search text) only exists for extracted heavy formats.
|
|
||||||
const hasExtracted = computed(() => node.value?.mode === 'extracted' && d.value?.content != null)
|
|
||||||
|
|
||||||
const extractedHtml = computed(() =>
|
|
||||||
d.value?.content ? marked.parse(d.value.content) : '')
|
|
||||||
|
|
||||||
// Right pane text (for text-native originals rendered inline).
|
|
||||||
const inlineText = computed(() => d.value?.text ?? '')
|
|
||||||
const inlineMarkdown = computed(() =>
|
|
||||||
viewerKind.value === 'markdown' && inlineText.value ? marked.parse(inlineText.value) : '')
|
|
||||||
|
|
||||||
const metaRows = computed(() => {
|
const metaRows = computed(() => {
|
||||||
const m = d.value?.meta as Record<string, any> | null
|
const m = d.value?.meta as Record<string, any> | null
|
||||||
@@ -54,11 +37,10 @@ const metaRows = computed(() => {
|
|||||||
<span class="badge" :data-mode="node.mode">{{ node.mode }}</span>
|
<span class="badge" :data-mode="node.mode">{{ node.mode }}</span>
|
||||||
<span class="name">{{ node.path }}</span>
|
<span class="name">{{ node.path }}</span>
|
||||||
<span class="dim">{{ node.ext }} · {{ humanBytes(node.bytes) }}</span>
|
<span class="dim">{{ node.ext }} · {{ humanBytes(node.bytes) }}</span>
|
||||||
<a class="dl" :href="d?.originalUrl" :download="node.name">↓ original</a>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- extracted: search text (left, secondary) | native original (right) -->
|
<!-- extracted: search text (left, secondary) | native original (right) -->
|
||||||
<SplitPane v-if="hasExtracted" class="body" :initial-size="1" :min="0.3" :max="3">
|
<SplitPane v-if="hasExtracted" class="body" :initial-size="1" :min="0.25" :max="3">
|
||||||
<template #first>
|
<template #first>
|
||||||
<div class="pane">
|
<div class="pane">
|
||||||
<div class="pane-label">Extracted text · search index (not the document)</div>
|
<div class="pane-label">Extracted text · search index (not the document)</div>
|
||||||
@@ -66,35 +48,13 @@ const metaRows = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #second>
|
<template #second>
|
||||||
<div class="pane viewer">
|
<NativeViewer :node="node" :original-url="d!.originalUrl" :content="d?.content" :text="d?.text" />
|
||||||
<template v-if="viewerKind === 'pdf'">
|
|
||||||
<iframe class="frame" :src="d?.originalUrl" title="pdf" />
|
|
||||||
</template>
|
|
||||||
<div v-else class="office">
|
|
||||||
<p>No inline preview for <b>.{{ node.ext }}</b>.</p>
|
|
||||||
<a class="btn" :href="d?.originalUrl" :download="node.name">Download original</a>
|
|
||||||
<p class="dim">The extracted text on the left is a search aid, not a faithful render.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
|
|
||||||
<!-- non-extracted: single native viewer of the original -->
|
<!-- link / pptx / meeting: single native viewer -->
|
||||||
<div v-else class="body single">
|
<div v-else class="body single">
|
||||||
<div v-if="viewerKind === 'image'" class="viewer center">
|
<NativeViewer :node="node" :original-url="d!.originalUrl" :content="d?.content" :text="d?.text" />
|
||||||
<img :src="d?.originalUrl" :alt="node.name" />
|
|
||||||
</div>
|
|
||||||
<iframe v-else-if="viewerKind === 'html'" class="frame" :src="d?.originalUrl" sandbox="" title="html" />
|
|
||||||
<div v-else-if="viewerKind === 'markdown'" class="md pad" v-html="inlineMarkdown" />
|
|
||||||
<pre v-else-if="viewerKind === 'text'" class="mono">{{ inlineText }}</pre>
|
|
||||||
<div v-else-if="viewerKind === 'video'" class="viewer center meeting">
|
|
||||||
<video class="video" :src="d?.originalUrl" controls />
|
|
||||||
<p class="dim">Meeting — its transcript is produced by meetus, not doocus.</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="office">
|
|
||||||
<p>No inline preview for <b>.{{ node.ext }}</b>.</p>
|
|
||||||
<a class="btn" :href="d?.originalUrl" :download="node.name">Download original</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer v-if="metaRows.length" class="meta">
|
<footer v-if="metaRows.length" class="meta">
|
||||||
@@ -112,7 +72,6 @@ const metaRows = computed(() => {
|
|||||||
}
|
}
|
||||||
.head .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
|
.head .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.head .dim, .dim { color: var(--text-secondary); font-size: var(--font-size-sm); }
|
.head .dim, .dim { color: var(--text-secondary); font-size: var(--font-size-sm); }
|
||||||
.head .dl { margin-left: auto; }
|
|
||||||
.badge {
|
.badge {
|
||||||
font-size: 10px; text-transform: uppercase; padding: 1px 5px; border-radius: 3px;
|
font-size: 10px; text-transform: uppercase; padding: 1px 5px; border-radius: 3px;
|
||||||
background: var(--surface-3); color: var(--text-secondary);
|
background: var(--surface-3); color: var(--text-secondary);
|
||||||
@@ -120,22 +79,13 @@ const metaRows = computed(() => {
|
|||||||
.badge[data-mode='extracted'] { background: var(--status-live, #2a6); color: #fff; }
|
.badge[data-mode='extracted'] { background: var(--status-live, #2a6); color: #fff; }
|
||||||
.badge[data-mode='meeting'] { background: var(--status-processing, #a60); color: #fff; }
|
.badge[data-mode='meeting'] { background: var(--status-processing, #a60); color: #fff; }
|
||||||
.body { flex: 1; min-height: 0; }
|
.body { flex: 1; min-height: 0; }
|
||||||
.body.single { overflow: auto; }
|
.body.single { overflow: hidden; }
|
||||||
.pane { height: 100%; display: flex; flex-direction: column; overflow: hidden; }
|
.pane { height: 100%; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
.pane-label {
|
.pane-label {
|
||||||
font-size: 11px; color: var(--text-secondary); padding: var(--space-1) var(--space-3);
|
font-size: 11px; color: var(--text-secondary); padding: var(--space-1) var(--space-3);
|
||||||
border-bottom: 1px solid var(--surface-2); flex-shrink: 0;
|
border-bottom: 1px solid var(--surface-2); flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.md { padding: var(--space-3); overflow: auto; }
|
.md { padding: var(--space-3); overflow: auto; }
|
||||||
.md.pad { padding: var(--space-4); }
|
|
||||||
.viewer { height: 100%; overflow: auto; }
|
|
||||||
.viewer.center { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-3); }
|
|
||||||
.frame { width: 100%; height: 100%; border: 0; }
|
|
||||||
.viewer img { max-width: 100%; }
|
|
||||||
.video { max-width: 100%; max-height: 70vh; }
|
|
||||||
.mono { margin: 0; padding: var(--space-4); white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-mono, monospace); font-size: var(--font-size-sm); }
|
|
||||||
.office { padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); align-items: flex-start; }
|
|
||||||
.btn { padding: var(--space-1) var(--space-3); background: var(--surface-2); border: var(--panel-border); border-radius: var(--panel-radius); }
|
|
||||||
.meta { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-top: var(--panel-border); flex-shrink: 0; }
|
.meta { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-top: var(--panel-border); flex-shrink: 0; }
|
||||||
.chip { font-size: var(--font-size-sm); color: var(--text-secondary); }
|
.chip { font-size: var(--font-size-sm); color: var(--text-secondary); }
|
||||||
.chip b { color: var(--text-primary); }
|
.chip b { color: var(--text-primary); }
|
||||||
|
|||||||
112
ui/doocus-app/src/components/NativeViewer.vue
Normal file
112
ui/doocus-app/src/components/NativeViewer.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
import type { FileNode } from '@/store'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
node: FileNode
|
||||||
|
originalUrl: string
|
||||||
|
content?: string | null // extracted text (pptx fallback render)
|
||||||
|
text?: string | null // inline text for text-native originals
|
||||||
|
}>()
|
||||||
|
|
||||||
|
type Kind = 'pdf' | 'image' | 'html' | 'markdown' | 'text' | 'docx' | 'xlsx' | 'pptx' | 'video' | 'none'
|
||||||
|
|
||||||
|
const kind = computed<Kind>(() => {
|
||||||
|
const e = props.node.ext
|
||||||
|
if (e === 'pdf') return 'pdf'
|
||||||
|
if (['png', 'jpg', 'jpeg'].includes(e)) return 'image'
|
||||||
|
if (['html', 'htm'].includes(e)) return 'html'
|
||||||
|
if (['md', 'markdown'].includes(e)) return 'markdown'
|
||||||
|
if (['txt', 'json', 'yaml', 'yml', 'csv'].includes(e)) return 'text'
|
||||||
|
if (e === 'docx') return 'docx'
|
||||||
|
if (e === 'xlsx') return 'xlsx'
|
||||||
|
if (e === 'pptx') return 'pptx'
|
||||||
|
if (e === 'mp4') return 'video'
|
||||||
|
return 'none'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Async-rendered HTML for docx (mammoth) and xlsx (SheetJS).
|
||||||
|
const rendered = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadError = ref('')
|
||||||
|
|
||||||
|
async function loadRich(): Promise<void> {
|
||||||
|
rendered.value = ''
|
||||||
|
loadError.value = ''
|
||||||
|
if (kind.value !== 'docx' && kind.value !== 'xlsx') return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await fetch(props.originalUrl)
|
||||||
|
const buf = await res.arrayBuffer()
|
||||||
|
if (kind.value === 'docx') {
|
||||||
|
const mammoth = await import('mammoth/mammoth.browser')
|
||||||
|
rendered.value = (await mammoth.convertToHtml({ arrayBuffer: buf })).value
|
||||||
|
} else {
|
||||||
|
const XLSX = await import('xlsx')
|
||||||
|
const wb = XLSX.read(new Uint8Array(buf), { type: 'array' })
|
||||||
|
rendered.value = wb.SheetNames.map((name) =>
|
||||||
|
`<h3>${name}</h3>` + XLSX.utils.sheet_to_html(wb.Sheets[name])).join('\n')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loadError.value = String(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.node.path, loadRich, { immediate: true })
|
||||||
|
|
||||||
|
const mdHtml = computed(() => (props.text ? marked.parse(props.text) : ''))
|
||||||
|
const pptxHtml = computed(() => (props.content ? marked.parse(props.content) : ''))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="nv">
|
||||||
|
<iframe v-if="kind === 'pdf'" class="frame" :src="originalUrl" title="pdf" />
|
||||||
|
|
||||||
|
<div v-else-if="kind === 'image'" class="center"><img :src="originalUrl" :alt="node.name" /></div>
|
||||||
|
|
||||||
|
<iframe v-else-if="kind === 'html'" class="frame" :src="originalUrl" sandbox="" title="html" />
|
||||||
|
|
||||||
|
<div v-else-if="kind === 'markdown'" class="doc md" v-html="mdHtml" />
|
||||||
|
|
||||||
|
<pre v-else-if="kind === 'text'" class="mono">{{ text }}</pre>
|
||||||
|
|
||||||
|
<div v-else-if="kind === 'video'" class="center">
|
||||||
|
<video class="video" :src="originalUrl" controls />
|
||||||
|
<p class="dim">Meeting — its transcript is produced by meetus, not doocus.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- docx / xlsx rendered client-side -->
|
||||||
|
<div v-else-if="kind === 'docx' || kind === 'xlsx'" class="doc rich">
|
||||||
|
<div v-if="loading" class="dim">Rendering…</div>
|
||||||
|
<div v-else-if="loadError" class="dim">Preview failed: {{ loadError }}</div>
|
||||||
|
<div v-else v-html="rendered" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- pptx: no faithful client render; show the per-slide extracted text -->
|
||||||
|
<div v-else-if="kind === 'pptx'" class="doc md">
|
||||||
|
<div class="dim label">Slide text (no visual render)</div>
|
||||||
|
<div v-html="pptxHtml" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="center dim">No preview for .{{ node.ext }}.</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.nv { height: 100%; overflow: auto; }
|
||||||
|
.frame { width: 100%; height: 100%; border: 0; }
|
||||||
|
.center { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--space-2); padding: var(--space-3); }
|
||||||
|
.center img { max-width: 100%; }
|
||||||
|
.video { max-width: 100%; max-height: 70vh; }
|
||||||
|
.doc { padding: var(--space-4); }
|
||||||
|
.doc.rich :deep(table) { border-collapse: collapse; }
|
||||||
|
.doc.rich :deep(td), .doc.rich :deep(th) { border: 1px solid var(--surface-3); padding: 2px 6px; }
|
||||||
|
.doc.rich :deep(h3) { margin: var(--space-3) 0 var(--space-1); }
|
||||||
|
.md :deep(img) { max-width: 100%; }
|
||||||
|
.mono { margin: 0; padding: var(--space-4); white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-mono, monospace); font-size: var(--font-size-sm); }
|
||||||
|
.dim { color: var(--text-secondary); }
|
||||||
|
.label { font-size: 11px; margin-bottom: var(--space-2); }
|
||||||
|
</style>
|
||||||
7
ui/doocus-app/src/env.d.ts
vendored
7
ui/doocus-app/src/env.d.ts
vendored
@@ -5,3 +5,10 @@ declare module '*.vue' {
|
|||||||
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
|
const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
|
||||||
export default component
|
export default component
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mammoth ships a browser bundle without bundled types for that entry point.
|
||||||
|
declare module 'mammoth/mammoth.browser' {
|
||||||
|
export function convertToHtml(
|
||||||
|
input: { arrayBuffer: ArrayBuffer },
|
||||||
|
): Promise<{ value: string; messages: unknown[] }>
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user