Revert "Desktop: Add OneNote Importer (#10642)"
This reverts commit b248700e28
.
We need to find a solution to make building the Rust package optional
@ -85,7 +85,6 @@ plugin_types/
|
||||
readme/
|
||||
packages/react-native-vosk/lib/
|
||||
packages/lib/countable/Countable.js
|
||||
packages/onenote-converter/pkg/onenote_converter.js
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
packages/app-cli/app/LinkSelector.js
|
||||
@ -1175,8 +1174,6 @@ packages/lib/services/interop/InteropService_Importer_Md.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_Md.js
|
||||
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js
|
||||
packages/lib/services/interop/InteropService_Importer_OneNote.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_OneNote.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||
packages/lib/services/interop/Module.test.js
|
||||
|
1
.github/scripts/run_ci.sh
vendored
@ -67,7 +67,6 @@ echo "IS_MACOS=$IS_MACOS"
|
||||
echo "Node $( node -v )"
|
||||
echo "Npm $( npm -v )"
|
||||
echo "Yarn $( yarn -v )"
|
||||
echo "Rust $( rustc --version )"
|
||||
|
||||
# =============================================================================
|
||||
# Install packages
|
||||
|
2
.github/workflows/build-android.yml
vendored
@ -26,8 +26,6 @@ jobs:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
corepack enable
|
||||
|
1
.github/workflows/github-actions-main.yml
vendored
@ -69,7 +69,6 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: olegtarasov/get-tag@v2.1.3
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||
|
2
.gitignore
vendored
@ -1151,8 +1151,6 @@ packages/lib/services/interop/InteropService_Importer_Md.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_Md.js
|
||||
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js
|
||||
packages/lib/services/interop/InteropService_Importer_OneNote.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_OneNote.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||
packages/lib/services/interop/Module.test.js
|
||||
|
@ -35,9 +35,6 @@ COPY packages/utils ./packages/utils
|
||||
COPY packages/lib ./packages/lib
|
||||
COPY packages/server ./packages/server
|
||||
|
||||
# We don't want to build onenote-converter since it is not used by the server
|
||||
RUN sed --in-place '/onenote-converter/d' ./packages/lib/package.json
|
||||
|
||||
# For some reason there's both a .yarn/cache and .yarn/berry/cache that are
|
||||
# being generated, and both have the same content. Not clear why it does this
|
||||
# but we can delete it anyway. We can delete the cache because we use
|
||||
|
@ -5,9 +5,6 @@
|
||||
},
|
||||
],
|
||||
"settings": {
|
||||
"rust-analyzer.linkedProjects": [
|
||||
"./packages/onenote-converter/Cargo.toml",
|
||||
],
|
||||
"files.exclude": {
|
||||
"_mydocs/mdtest/": true,
|
||||
"_releases/": true,
|
||||
|
@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
|
||||
<div class="container-outline" style="left: 48px;position: absolute;top: 107px;width: 624px;">
|
||||
<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
.small {
|
||||
font: italic 13px sans-serif;
|
||||
}
|
||||
.heavy {
|
||||
font: bold 30px sans-serif;
|
||||
}
|
||||
/* Note that the color of the text is set with the *
|
||||
* fill property, the color property is for HTML only */
|
||||
.Rrrrr {
|
||||
font: italic 40px serif;
|
||||
fill: red;
|
||||
}
|
||||
</style>
|
||||
<text x="20" y="35" class="small">My</text>
|
||||
<text x="40" y="35" class="heavy">cat</text>
|
||||
<text x="55" y="55" class="small">is</text>
|
||||
<text x="65" y="55" class="Rrrrr">Grumpy!</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -25,7 +25,6 @@ const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
import Navigator from './Navigator';
|
||||
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
||||
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
|
||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||
import WindowCommandsAndDialogs from './WindowCommandsAndDialogs/WindowCommandsAndDialogs';
|
||||
import { defaultWindowId, stateUtils, WindowState } from '@joplin/lib/reducer';
|
||||
import bridge from '../services/bridge';
|
||||
@ -92,9 +91,6 @@ async function initialize() {
|
||||
type: 'NOTE_VISIBLE_PANES_SET',
|
||||
panes: Setting.value('noteVisiblePanes'),
|
||||
});
|
||||
|
||||
InteropService.instance().document = document;
|
||||
InteropService.instance().xmlSerializer = new XMLSerializer();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
@ -1,11 +1,11 @@
|
||||
import paginationToSql from './models/utils/paginationToSql';
|
||||
import Database from './database';
|
||||
import uuid from './uuid';
|
||||
import time from './time';
|
||||
import JoplinDatabase, { TableField } from './JoplinDatabase';
|
||||
import { LoadOptions, SaveOptions } from './models/utils/types';
|
||||
import ActionLogger, { ItemActionType as ItemActionType } from './utils/ActionLogger';
|
||||
import { BaseItemEntity, SqlQuery } from './services/database/types';
|
||||
import uuid from './uuid';
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
// New code should make use of this enum
|
||||
@ -80,8 +80,6 @@ class BaseModel {
|
||||
['TYPE_COMMAND', ModelType.Command],
|
||||
];
|
||||
|
||||
private static uuidGenerator: ()=> string = uuid.create;
|
||||
|
||||
public static TYPE_NOTE = ModelType.Note;
|
||||
public static TYPE_FOLDER = ModelType.Folder;
|
||||
public static TYPE_SETTING = ModelType.Setting;
|
||||
@ -578,7 +576,7 @@ class BaseModel {
|
||||
|
||||
if (options.isNew) {
|
||||
if (this.useUuid() && !o.id) {
|
||||
modelId = this.generateUuid();
|
||||
modelId = uuid.create();
|
||||
o.id = modelId;
|
||||
}
|
||||
|
||||
@ -759,15 +757,6 @@ class BaseModel {
|
||||
return this.db_;
|
||||
}
|
||||
|
||||
public static generateUuid() {
|
||||
return this.uuidGenerator();
|
||||
}
|
||||
|
||||
public static setIdGenerator(generator: ()=> string) {
|
||||
const previous = this.uuidGenerator;
|
||||
this.uuidGenerator = generator;
|
||||
return previous;
|
||||
}
|
||||
// static isReady() {
|
||||
// return !!this.db_;
|
||||
// }
|
||||
|
@ -17,11 +17,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/adm-zip": "0.5.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsdom": "21.1.6",
|
||||
"@types/markdown-it": "13.0.9",
|
||||
"@types/mustache": "4.2.5",
|
||||
"@types/node": "18.19.42",
|
||||
@ -31,7 +29,6 @@
|
||||
"canvas": "2.11.2",
|
||||
"clean-html": "1.5.0",
|
||||
"jest": "29.7.0",
|
||||
"jsdom": "23.2.0",
|
||||
"pdfjs-dist": "3.11.174",
|
||||
"react": "18.3.1",
|
||||
"react-test-renderer": "18.3.1",
|
||||
@ -47,13 +44,11 @@
|
||||
"@joplin/fork-sax": "^1.2.56",
|
||||
"@joplin/fork-uslug": "^1.0.17",
|
||||
"@joplin/htmlpack": "~3.2",
|
||||
"@joplin/onenote-converter": "0.0.1",
|
||||
"@joplin/renderer": "~3.2",
|
||||
"@joplin/turndown": "^4.0.74",
|
||||
"@joplin/turndown-plugin-gfm": "^1.0.56",
|
||||
"@joplin/utils": "~3.2",
|
||||
"@types/nanoid": "3.0.0",
|
||||
"adm-zip": "0.5.12",
|
||||
"async-mutex": "0.5.0",
|
||||
"base-64": "1.0.0",
|
||||
"base64-stream": "1.0.0",
|
||||
|
@ -30,8 +30,6 @@ export default class InteropService {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
private eventEmitter_: any = null;
|
||||
private static instance_: InteropService;
|
||||
private document_: Document;
|
||||
private xmlSerializer_: XMLSerializer;
|
||||
|
||||
public static instance(): InteropService {
|
||||
if (!this.instance_) this.instance_ = new InteropService();
|
||||
@ -135,14 +133,6 @@ export default class InteropService {
|
||||
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
|
||||
description: _('Text document'),
|
||||
}, () => new InteropService_Importer_Md()),
|
||||
|
||||
makeImportModule({
|
||||
format: 'zip',
|
||||
fileExtensions: ['zip'],
|
||||
sources: [FileSystemItem.File],
|
||||
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
|
||||
description: _('OneNote Notebook'),
|
||||
}, dynamicRequireModuleFactory('./InteropService_Importer_OneNote')),
|
||||
];
|
||||
|
||||
const exportModules = [
|
||||
@ -199,22 +189,6 @@ export default class InteropService {
|
||||
this.eventEmitter_.emit('modulesChanged');
|
||||
}
|
||||
|
||||
public set xmlSerializer(xmlSerializer: XMLSerializer) {
|
||||
this.xmlSerializer_ = xmlSerializer;
|
||||
}
|
||||
|
||||
public get xmlSerializer() {
|
||||
return this.xmlSerializer_;
|
||||
}
|
||||
|
||||
public set document(document: Document) {
|
||||
this.document_ = document;
|
||||
}
|
||||
|
||||
public get document() {
|
||||
return this.document_;
|
||||
}
|
||||
|
||||
// Find the module that matches the given type ("importer" or "exporter")
|
||||
// and the given format. Some formats can have multiple associated importers
|
||||
// or exporters, such as ENEX. In this case, the one marked as "isDefault"
|
||||
@ -299,8 +273,6 @@ export default class InteropService {
|
||||
format: 'auto',
|
||||
destinationFolderId: null,
|
||||
destinationFolder: null,
|
||||
xmlSerializer: this.xmlSerializer,
|
||||
document: this.document,
|
||||
...options,
|
||||
};
|
||||
|
||||
|
@ -1,176 +0,0 @@
|
||||
import Note from '../../models/Note';
|
||||
import Folder from '../../models/Folder';
|
||||
import { remove, readFile } from 'fs-extra';
|
||||
import { createTempDir, setupDatabaseAndSynchronizer, supportDir, switchClient } from '../../testing/test-utils';
|
||||
import { NoteEntity } from '../database/types';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
import BaseModel from '../../BaseModel';
|
||||
import InteropService from './InteropService';
|
||||
import InteropService_Importer_OneNote from './InteropService_Importer_OneNote';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { ImportModuleOutputFormat } from './types';
|
||||
|
||||
describe('InteropService_Importer_OneNote', () => {
|
||||
let tempDir: string;
|
||||
async function importNote(path: string) {
|
||||
const newFolder = await Folder.save({ title: 'folder' });
|
||||
const service = InteropService.instance();
|
||||
await service.import({
|
||||
outputFormat: ImportModuleOutputFormat.Markdown,
|
||||
path,
|
||||
destinationFolder: newFolder,
|
||||
destinationFolderId: newFolder.id,
|
||||
});
|
||||
const allNotes: NoteEntity[] = await Note.all();
|
||||
return allNotes;
|
||||
}
|
||||
beforeAll(() => {
|
||||
const jsdom = new JSDOM('<div></div>');
|
||||
InteropService.instance().document = jsdom.window.document;
|
||||
InteropService.instance().xmlSerializer = new jsdom.window.XMLSerializer();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
tempDir = await createTempDir();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await remove(tempDir);
|
||||
});
|
||||
it('should import a simple OneNote notebook', async () => {
|
||||
const notes = await importNote(`${supportDir}/onenote/simple_notebook.zip`);
|
||||
const folders = await Folder.all();
|
||||
|
||||
expect(notes.length).toBe(2);
|
||||
const mainNote = notes[0];
|
||||
|
||||
expect(folders.length).toBe(3);
|
||||
const parentFolder = folders.find(f => f.id === mainNote.parent_id);
|
||||
expect(parentFolder.title).toBe('Section title');
|
||||
expect(folders.find(f => f.id === parentFolder.parent_id).title).toBe('Simple notebook');
|
||||
|
||||
expect(mainNote.title).toBe('Page title');
|
||||
expect(mainNote.markup_language).toBe(MarkupToHtml.MARKUP_LANGUAGE_HTML);
|
||||
expect(mainNote.body).toMatchSnapshot(mainNote.title);
|
||||
});
|
||||
|
||||
it('should preserve indentation of subpages in Section page', async () => {
|
||||
const notes = await importNote(`${supportDir}/onenote/subpages.zip`);
|
||||
|
||||
const sectionPage = notes.find(n => n.title === 'Section');
|
||||
const menuHtml = sectionPage.body.split('<ul>')[1].split('</ul>')[0];
|
||||
const menuLines = menuHtml.split('</li>');
|
||||
|
||||
const pageTwo = notes.find(n => n.title === 'Page 2');
|
||||
expect(menuLines[3].trim()).toBe(`<li class="l1"><a href=":/${pageTwo.id}" target="content" title="Page 2">${pageTwo.title}</a>`);
|
||||
|
||||
const pageTwoA = notes.find(n => n.title === 'Page 2-a');
|
||||
expect(menuLines[4].trim()).toBe(`<li class="l2"><a href=":/${pageTwoA.id}" target="content" title="Page 2-a">${pageTwoA.title}</a>`);
|
||||
|
||||
const pageTwoAA = notes.find(n => n.title === 'Page 2-a-a');
|
||||
expect(menuLines[5].trim()).toBe(`<li class="l3"><a href=":/${pageTwoAA.id}" target="content" title="Page 2-a-a">${pageTwoAA.title}</a>`);
|
||||
|
||||
const pageTwoB = notes.find(n => n.title === 'Page 2-b');
|
||||
expect(menuLines[7].trim()).toBe(`<li class="l2"><a href=":/${pageTwoB.id}" target="content" title="Page 2-b">${pageTwoB.title}</a>`);
|
||||
});
|
||||
|
||||
it('should created subsections', async () => {
|
||||
const notes = await importNote(`${supportDir}/onenote/subsections.zip`);
|
||||
const folders = await Folder.all();
|
||||
|
||||
const parentSection = folders.find(f => f.title === 'Group Section 1');
|
||||
const subSection = folders.find(f => f.title === 'Group Section 1-a');
|
||||
const subSection1 = folders.find(f => f.title === 'Subsection 1');
|
||||
const subSection2 = folders.find(f => f.title === 'Subsection 2');
|
||||
const notesFromParentSection = notes.filter(n => n.parent_id === parentSection.id);
|
||||
|
||||
expect(parentSection.id).toBe(subSection1.parent_id);
|
||||
expect(parentSection.id).toBe(subSection2.parent_id);
|
||||
expect(parentSection.id).toBe(subSection.parent_id);
|
||||
expect(folders.length).toBe(7);
|
||||
expect(notes.length).toBe(6);
|
||||
expect(notesFromParentSection.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should expect notes to be rendered the same', async () => {
|
||||
let idx = 0;
|
||||
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
|
||||
const notes = await importNote(`${supportDir}/onenote/complex_notes.zip`);
|
||||
|
||||
const folders = await Folder.all();
|
||||
const parentSection = folders.find(f => f.title === 'Quick Notes');
|
||||
expect(folders.length).toBe(3);
|
||||
expect(notes.length).toBe(7);
|
||||
expect(notes.filter(n => n.parent_id === parentSection.id).length).toBe(6);
|
||||
|
||||
for (const note of notes) {
|
||||
expect(note.body).toMatchSnapshot(note.title);
|
||||
}
|
||||
BaseModel.setIdGenerator(originalIdGenerator);
|
||||
});
|
||||
|
||||
it('should render the proper tree for notebook with group sections', async () => {
|
||||
const notes = await importNote(`${supportDir}/onenote/group_sections.zip`);
|
||||
const folders = await Folder.all();
|
||||
|
||||
const mainFolder = folders.find(f => f.title === 'Notebook created on OneNote App');
|
||||
const section = folders.find(f => f.title === 'Section');
|
||||
const sectionA1 = folders.find(f => f.title === 'Section A1');
|
||||
const sectionA = folders.find(f => f.title === 'Section A');
|
||||
const sectionB1 = folders.find(f => f.title === 'Section B1');
|
||||
const sectionB = folders.find(f => f.title === 'Section B');
|
||||
const sectionD1 = folders.find(f => f.title === 'Section D1');
|
||||
const sectionD = folders.find(f => f.title === 'Section D');
|
||||
|
||||
expect(section.parent_id).toBe(mainFolder.id);
|
||||
expect(sectionA.parent_id).toBe(mainFolder.id);
|
||||
expect(sectionD.parent_id).toBe(mainFolder.id);
|
||||
|
||||
expect(sectionA1.parent_id).toBe(sectionA.id);
|
||||
expect(sectionB.parent_id).toBe(sectionA.id);
|
||||
|
||||
expect(sectionB1.parent_id).toBe(sectionB.id);
|
||||
expect(sectionD1.parent_id).toBe(sectionD.id);
|
||||
|
||||
expect(notes.filter(n => n.parent_id === sectionA1.id).length).toBe(2);
|
||||
expect(notes.filter(n => n.parent_id === sectionB1.id).length).toBe(2);
|
||||
expect(notes.filter(n => n.parent_id === sectionD1.id).length).toBe(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'svg_with_text_and_style.html',
|
||||
'many_svgs.html',
|
||||
])('should extract svgs', async (filename: string) => {
|
||||
const titleGenerator = () => {
|
||||
let id = 0;
|
||||
return () => {
|
||||
id += 1;
|
||||
return `id${id}`;
|
||||
};
|
||||
};
|
||||
const filepath = `${supportDir}/onenote/${filename}`;
|
||||
const content = await readFile(filepath, 'utf-8');
|
||||
|
||||
const jsdom = new JSDOM('<div></div>');
|
||||
InteropService.instance().document = jsdom.window.document;
|
||||
InteropService.instance().xmlSerializer = new jsdom.window.XMLSerializer();
|
||||
|
||||
const importer = new InteropService_Importer_OneNote();
|
||||
await importer.init('asdf', {
|
||||
document: jsdom.window.document,
|
||||
xmlSerializer: new jsdom.window.XMLSerializer(),
|
||||
});
|
||||
|
||||
expect(importer.extractSvgs(content, titleGenerator())).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should ignore broken characters at the start of paragraph', async () => {
|
||||
let idx = 0;
|
||||
const originalIdGenerator = BaseModel.setIdGenerator(() => String(idx++));
|
||||
const notes = await importNote(`${supportDir}/onenote/bug_broken_character.zip`);
|
||||
|
||||
expect(notes.find(n => n.title === 'Action research - Wikipedia').body).toMatchSnapshot();
|
||||
|
||||
BaseModel.setIdGenerator(originalIdGenerator);
|
||||
});
|
||||
});
|
@ -1,157 +0,0 @@
|
||||
import { ImportExportResult, ImportModuleOutputFormat, ImportOptions } from './types';
|
||||
|
||||
import InteropService_Importer_Base from './InteropService_Importer_Base';
|
||||
import { NoteEntity } from '../database/types';
|
||||
import { rtrimSlashes } from '../../path-utils';
|
||||
import * as AdmZip from 'adm-zip';
|
||||
import InteropService_Importer_Md from './InteropService_Importer_Md';
|
||||
import { join, resolve, normalize, sep, dirname } from 'path';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import { uuidgen } from '../../uuid';
|
||||
import shim from '../../shim';
|
||||
|
||||
const logger = Logger.create('InteropService_Importer_OneNote');
|
||||
|
||||
export type SvgXml = {
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
type ExtractSvgsReturn = {
|
||||
svgs: SvgXml[];
|
||||
html: string;
|
||||
};
|
||||
|
||||
// See onenote-converter README.md for more information
|
||||
export default class InteropService_Importer_OneNote extends InteropService_Importer_Base {
|
||||
protected importedNotes: Record<string, NoteEntity> = {};
|
||||
private document: Document = null;
|
||||
private xmlSerializer: XMLSerializer = null;
|
||||
|
||||
public async init(sourcePath: string, options: ImportOptions) {
|
||||
await super.init(sourcePath, options);
|
||||
if (!options.document || !options.xmlSerializer) {
|
||||
throw new Error('OneNote importer requires document and XMLSerializer to be able to extract SVG from HTML.');
|
||||
}
|
||||
this.document = options.document;
|
||||
this.xmlSerializer = options.xmlSerializer;
|
||||
}
|
||||
|
||||
private getEntryDirectory(unzippedPath: string, entryName: string) {
|
||||
const withoutBasePath = entryName.replace(unzippedPath, '');
|
||||
return normalize(withoutBasePath).split(sep)[0];
|
||||
}
|
||||
|
||||
public async exec(result: ImportExportResult) {
|
||||
const sourcePath = rtrimSlashes(this.sourcePath_);
|
||||
const unzipTempDirectory = await this.temporaryDirectory_(true);
|
||||
const zip = new AdmZip(sourcePath);
|
||||
logger.info('Unzipping files...');
|
||||
zip.extractAllTo(unzipTempDirectory, false);
|
||||
|
||||
const files = zip.getEntries();
|
||||
if (files.length === 0) {
|
||||
result.warnings.push('Zip file has no files.');
|
||||
return result;
|
||||
}
|
||||
|
||||
const tempOutputDirectory = await this.temporaryDirectory_(true);
|
||||
const baseFolder = this.getEntryDirectory(unzipTempDirectory, files[0].entryName);
|
||||
const notebookBaseDir = join(unzipTempDirectory, baseFolder, sep);
|
||||
const outputDirectory2 = join(tempOutputDirectory, baseFolder);
|
||||
|
||||
const notebookFiles = zip.getEntries().filter(e => e.name !== '.onetoc2' && e.name !== 'OneNote_RecycleBin.onetoc2');
|
||||
const { oneNoteConverter } = shim.requireDynamic('../../../onenote-converter/pkg/onenote_converter');
|
||||
|
||||
logger.info('Extracting OneNote to HTML');
|
||||
for (const notebookFile of notebookFiles) {
|
||||
const notebookFilePath = join(unzipTempDirectory, notebookFile.entryName);
|
||||
try {
|
||||
await oneNoteConverter(notebookFilePath, resolve(outputDirectory2), notebookBaseDir);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Extracting SVGs into files');
|
||||
await this.moveSvgToLocalFile(tempOutputDirectory);
|
||||
|
||||
logger.info('Importing HTML into Joplin');
|
||||
const importer = new InteropService_Importer_Md();
|
||||
importer.setMetadata({ fileExtensions: ['html'] });
|
||||
await importer.init(tempOutputDirectory, {
|
||||
...this.options_,
|
||||
format: 'html',
|
||||
outputFormat: ImportModuleOutputFormat.Html,
|
||||
|
||||
});
|
||||
logger.info('Finished');
|
||||
result = await importer.exec(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async moveSvgToLocalFile(baseFolder: string) {
|
||||
const htmlFiles = await this.getValidHtmlFiles(resolve(baseFolder));
|
||||
|
||||
for (const file of htmlFiles) {
|
||||
const fileLocation = join(baseFolder, file.path);
|
||||
const originalHtml = await shim.fsDriver().readFile(fileLocation);
|
||||
const { svgs, html: updatedHtml } = this.extractSvgs(originalHtml, () => uuidgen(10));
|
||||
|
||||
if (!svgs || !svgs.length) continue;
|
||||
|
||||
await shim.fsDriver().writeFile(fileLocation, updatedHtml, 'utf8');
|
||||
await this.createSvgFiles(svgs, join(baseFolder, dirname(file.path)));
|
||||
}
|
||||
}
|
||||
|
||||
private async getValidHtmlFiles(baseFolder: string) {
|
||||
const files = await shim.fsDriver().readDirStats(baseFolder, { recursive: true });
|
||||
const htmlFiles = files.filter(f => !f.isDirectory() && f.path.endsWith('.html'));
|
||||
return htmlFiles;
|
||||
}
|
||||
|
||||
private async createSvgFiles(svgs: SvgXml[], svgBaseFolder: string) {
|
||||
for (const svg of svgs) {
|
||||
await shim.fsDriver().writeFile(join(svgBaseFolder, svg.title), svg.content, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
public extractSvgs(html: string, titleGenerator: ()=> string): ExtractSvgsReturn {
|
||||
const htmlDocument = this.document.implementation.createHTMLDocument('htmlDocument');
|
||||
const root = htmlDocument.createElement('html');
|
||||
const body = htmlDocument.createElement('body');
|
||||
root.appendChild(body);
|
||||
root.innerHTML = html;
|
||||
|
||||
// get all "top-level" SVGS (ignore nested)
|
||||
const svgNodeList = root.querySelectorAll('svg');
|
||||
|
||||
if (!svgNodeList || !svgNodeList.length) {
|
||||
return { svgs: [], html };
|
||||
}
|
||||
|
||||
const svgs: SvgXml[] = [];
|
||||
|
||||
for (const svgNode of svgNodeList) {
|
||||
const title = `${titleGenerator()}.svg`;
|
||||
const img = htmlDocument.createElement('img');
|
||||
img.setAttribute('style', svgNode.getAttribute('style'));
|
||||
img.setAttribute('src', `./${title}`);
|
||||
svgNode.removeAttribute('style');
|
||||
|
||||
svgs.push({
|
||||
title,
|
||||
content: this.xmlSerializer.serializeToString(svgNode),
|
||||
});
|
||||
|
||||
svgNode.parentElement.replaceChild(img, svgNode);
|
||||
}
|
||||
|
||||
return {
|
||||
svgs,
|
||||
html: this.xmlSerializer.serializeToString(root),
|
||||
};
|
||||
}
|
||||
}
|
@ -51,8 +51,6 @@ export interface ImportOptions {
|
||||
onProgress?: (progressState: any, progress?: any)=> void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onError?: (error: any)=> void;
|
||||
document?: Document;
|
||||
xmlSerializer?: XMLSerializer;
|
||||
|
||||
defaultFolderTitle?: string;
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ import crypto from './services/e2ee/crypto';
|
||||
|
||||
import FileApiDriverLocal from './file-api-driver-local';
|
||||
import * as mimeUtils from './mime-utils';
|
||||
import BaseItem from './models/BaseItem';
|
||||
const { _ } = require('./locale');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
@ -310,11 +309,13 @@ function shimInit(options: ShimInitOptions = null) {
|
||||
|
||||
const isUpdate = !!options.destinationResourceId;
|
||||
|
||||
const uuid = require('./uuid').default;
|
||||
|
||||
if (!(await fs.pathExists(filePath))) throw new Error(_('Cannot access %s', filePath));
|
||||
|
||||
defaultProps = defaultProps ? defaultProps : {};
|
||||
|
||||
let resourceId = defaultProps.id ? defaultProps.id : BaseItem.generateUuid();
|
||||
let resourceId = defaultProps.id ? defaultProps.id : uuid.create();
|
||||
if (isUpdate) resourceId = options.destinationResourceId;
|
||||
|
||||
let resource = isUpdate ? {} : Resource.new();
|
||||
|
7
packages/onenote-converter/.gitignore
vendored
@ -1,7 +0,0 @@
|
||||
/target
|
||||
/output
|
||||
|
||||
/.idea
|
||||
*.iml
|
||||
|
||||
/pkg
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"rust-analyzer.linkedProjects": [
|
||||
"./Cargo.toml"
|
||||
]
|
||||
}
|
1040
packages/onenote-converter/Cargo.lock
generated
@ -1,41 +0,0 @@
|
||||
[package]
|
||||
name = "onenote-converter"
|
||||
version = "0.0.1"
|
||||
authors = ["Pedro Luiz <pedrlz.frn@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "Convert Microsoft OneNote® notebooks to HTML"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/laurent22/joplin"
|
||||
keywords = ["onenote"]
|
||||
|
||||
[dependencies]
|
||||
askama = "0.10"
|
||||
color-eyre = "0.5"
|
||||
log = "0.4.11"
|
||||
mime_guess = "2.0.3"
|
||||
once_cell = "1.4.1"
|
||||
palette = "0.5.0"
|
||||
percent-encoding = "2.1.0"
|
||||
regex = "1"
|
||||
sanitize-filename = "0.3.0"
|
||||
structopt = "0.3"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
bytes = "1.2.0"
|
||||
encoding_rs = "0.8.31"
|
||||
enum-primitive-derive = "0.2.2"
|
||||
itertools = "0.10.3"
|
||||
num-traits = "0.2"
|
||||
paste = "1.0"
|
||||
thiserror = "1.0"
|
||||
uuid = "1.1.2"
|
||||
widestring = "1.0.2"
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"console"
|
||||
]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
@ -1,66 +0,0 @@
|
||||
# OneNote Converter
|
||||
|
||||
This package is used to process OneNote backup files and output HTML that Joplin can import.
|
||||
|
||||
The code is based on the projects created by https://github.com/msiemens
|
||||
|
||||
We adapted it to target WebAssembly, adding Node.js functions that could interface with the host machine. For that to happen we are using custom-made functions (see `node_functions.js`) and the Node.js standard library (see `src/utils.rs`).
|
||||
|
||||
## How the OneNote Importer Process Works
|
||||
|
||||
The requirement for this project was to simplify the migration process from OneNote to Joplin. The starting point of this migration is to export the notebook from OneNote as a `zip` file containing files in the binary format used by OneNote.
|
||||
|
||||
The process looks like this:
|
||||
|
||||
1. Unzip the backup file.
|
||||
2. Use `onenote-converter` to read and convert the binary files to HTML (this project).
|
||||
3. Extract the SVG nodes from the HTML to resources:
|
||||
1. Find all SVG nodes in the HTML file.
|
||||
2. Create SVG files from the nodes.
|
||||
3. Update the HTML file with references to the SVGs.
|
||||
4. Use the Importer HTML service to create the Joplin notes and resources.
|
||||
|
||||
See the `InteropService_Importer_OneNote` class in the `lib` project for details.
|
||||
|
||||
### SVG Extraction
|
||||
|
||||
The OneNote drawing feature uses `<svg>` tags to save user drawings. Joplin doesn't support SVG rendering due to security concerns, so we added a step to extract the `<svg>` elements as SVG images, replacing them with `<img>` tags.
|
||||
|
||||
For each HTML file, we:
|
||||
|
||||
- Mount the HTML in the document.
|
||||
- Find all the `svg` nodes.
|
||||
- Replace each `svg` node with an `img` node that has a unique title, which will be used as the resource name.
|
||||
- After editing the entire document, update the HTML.
|
||||
- Create the SVG images on the local disk with the title used in the replaced `img` tags.
|
||||
|
||||
After this, the HTML should look the same and is ready to be imported by the Importer HTML service.
|
||||
|
||||
## Project structure:
|
||||
|
||||
```
|
||||
- onenote-converter
|
||||
- package.json -> where the project is built
|
||||
- node_functions.js -> where the custom-made functions used inside rust goes
|
||||
...
|
||||
- pkg -> artifact folder generated in the build step
|
||||
- onenote_converter.js -> main file
|
||||
...
|
||||
- src
|
||||
- lib.rs -> starting point
|
||||
```
|
||||
|
||||
## Development requirements:
|
||||
|
||||
To work with the project you will need:
|
||||
|
||||
- Rust https://www.rust-lang.org/learn/get-started
|
||||
|
||||
When working with the Rust code you will probably rather run `yarn buildDev` since it is faster and it has more logging messages (they can be disabled in the macro `log!()`)
|
||||
|
||||
During development, it will be easier to test it where this library is called. `InteropService_Importer_Onenote.ts` is the code that depends on this and already has some tests.
|
||||
|
||||
## Security concerns
|
||||
|
||||
We are using WebAssembly with Node.js calls to the file system, reading and writing files and directories, which means
|
||||
it is not isolated (no more than Node.js is, for that matter).
|
@ -1,2 +0,0 @@
|
||||
[general]
|
||||
dirs = ["src/templates"]
|
@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M16.172 11l-5.364-5.364 1.414-1.414L20 12l-7.778 7.778-1.414-1.414L16.172 13H4v-2z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 234 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M17 15.245v6.872a.5.5 0 0 1-.757.429L12 20l-4.243 2.546a.5.5 0 0 1-.757-.43v-6.87a8 8 0 1 1 10 0zm-8 1.173v3.05l3-1.8 3 1.8v-3.05A7.978 7.978 0 0 1 12 17a7.978 7.978 0 0 1-3-.582zM12 15a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 368 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M13 21v2h-2v-2H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h6a3.99 3.99 0 0 1 3 1.354A3.99 3.99 0 0 1 15 3h6a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-8zm7-2V5h-5a2 2 0 0 0-2 2v12h7zm-9 0V7a2 2 0 0 0-2-2H4v14h7z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 340 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M5.763 17H20V5H4v13.385L5.763 17zm.692 2L2 22.5V4a1 1 0 0 1 1-1h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6.455z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 256 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 218 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 172 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 260 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 223 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm1 2v14h14V5H5z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 238 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm7.003 13l7.07-7.071-1.414-1.414-5.656 5.657-2.829-2.829-1.414 1.414L11.003 16z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 302 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M19 7h5v2h-5V7zm-2 5h7v2h-7v-2zm3 5h4v2h-4v-2zM2 22a8 8 0 1 1 16 0h-2a6 6 0 1 0-12 0H2zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 358 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-1-5h2v2h-2v-2zm0-8h2v6h-2V7z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 290 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M20 22H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1zm-1-2V4H5v16h14zM8 7h8v2H8V7zm0 4h8v2H8v-2zm0 4h5v2H8v-2z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 282 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M2 3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993zM8 5v14h8V5H8zM4 5v2h2V5H4zm14 0v2h2V5h-2zM4 9v2h2V9H4zm14 0v2h2V9h-2zM4 13v2h2v-2H4zm14 0v2h2v-2h-2zM4 17v2h2v-2H4zm14 0v2h2v-2h-2z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 404 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M3 3h9.382a1 1 0 0 1 .894.553L14 5h6a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1h-6.382a1 1 0 0 1-.894-.553L12 16H5v6H3V3z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 261 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M19 21H5a1 1 0 0 1-1-1v-9H1l10.327-9.388a1 1 0 0 1 1.346 0L23 11h-3v9a1 1 0 0 1-1 1zm-6-2h5V9.157l-6-5.454-6 5.454V19h5v-6h2v6z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 279 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M9.973 18H11v-5h2v5h1.027c.132-1.202.745-2.194 1.74-3.277.113-.122.832-.867.917-.973a6 6 0 1 0-9.37-.002c.086.107.807.853.918.974.996 1.084 1.609 2.076 1.741 3.278zM10 20v1h4v-1h-4zm-4.246-5a8 8 0 1 1 12.49.002C17.624 15.774 16 17 16 18.5V21a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2.5C8 17 6.375 15.774 5.754 15z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 457 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M18.364 15.536L16.95 14.12l1.414-1.414a5 5 0 1 0-7.071-7.071L9.879 7.05 8.464 5.636 9.88 4.222a7 7 0 0 1 9.9 9.9l-1.415 1.414zm-2.828 2.828l-1.415 1.414a7 7 0 0 1-9.9-9.9l1.415-1.414L7.05 9.88l-1.414 1.414a5 5 0 1 0 7.071 7.071l1.414-1.414 1.415 1.414zm-.708-10.607l1.415 1.415-7.071 7.07-1.415-1.414 7.071-7.07z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 464 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M19 10h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V11a1 1 0 0 1 1-1h1V9a7 7 0 1 1 14 0v1zM5 12v8h14v-8H5zm6 2h2v4h-2v-4zm6-4V9A5 5 0 0 0 7 9v1h10z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 303 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M15.243 4.515l-6.738 6.737-.707 2.121-1.04 1.041 2.828 2.829 1.04-1.041 2.122-.707 6.737-6.738-4.242-4.242zm6.364 3.535a1 1 0 0 1 0 1.414l-7.779 7.779-2.12.707-1.415 1.414a1 1 0 0 1-1.414 0l-4.243-4.243a1 1 0 0 1 0-1.414l1.414-1.414.707-2.121 7.779-7.779a1 1 0 0 1 1.414 0l5.657 5.657zm-6.364-.707l1.414 1.414-4.95 4.95-1.414-1.414 4.95-4.95zM4.283 16.89l2.828 2.829-1.414 1.414-4.243-1.414 2.828-2.829z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 555 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M12 13.535V3h8v3h-6v11a4 4 0 1 1-2-3.465z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 193 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path fill-rule="nonzero" d="M9.366 10.682a10.556 10.556 0 0 0 3.952 3.952l.884-1.238a1 1 0 0 1 1.294-.296 11.422 11.422 0 0 0 4.583 1.364 1 1 0 0 1 .921.997v4.462a1 1 0 0 1-.898.995c-.53.055-1.064.082-1.602.082C9.94 21 3 14.06 3 5.5c0-.538.027-1.072.082-1.602A1 1 0 0 1 4.077 3h4.462a1 1 0 0 1 .997.921A11.422 11.422 0 0 0 10.9 8.504a1 1 0 0 1-.296 1.294l-1.238.884zm-2.522-.657l1.9-1.357A13.41 13.41 0 0 1 7.647 5H5.01c-.006.166-.009.333-.009.5C5 12.956 11.044 19 18.5 19c.167 0 .334-.003.5-.01v-2.637a13.41 13.41 0 0 1-3.668-1.097l-1.357 1.9a12.442 12.442 0 0 1-1.588-.75l-.058-.033a12.556 12.556 0 0 1-4.702-4.702l-.033-.058a12.442 12.442 0 0 1-.75-1.588z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 802 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0H24V24H0z"/>
|
||||
<path d="M12 19c.828 0 1.5.672 1.5 1.5S12.828 22 12 22s-1.5-.672-1.5-1.5.672-1.5 1.5-1.5zm0-17c3.314 0 6 2.686 6 6 0 2.165-.753 3.29-2.674 4.923C13.399 14.56 13 15.297 13 17h-2c0-2.474.787-3.695 3.031-5.601C15.548 10.11 16 9.434 16 8c0-2.21-1.79-4-4-4S8 5.79 8 8v1H6V8c0-3.314 2.686-6 6-6z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 432 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M3.741 1.408l18.462 10.154a.5.5 0 0 1 0 .876L3.741 22.592A.5.5 0 0 1 3 22.154V1.846a.5.5 0 0 1 .741-.438zM5 13v6.617L18.85 12 5 4.383V11h5v2H5z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 295 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18.26l-7.053 3.948 1.575-7.928L.587 8.792l8.027-.952L12 .5l3.386 7.34 8.027.952-5.935 5.488 1.575 7.928z"/></svg>
|
Before Width: | Height: | Size: 246 B |
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M4 22a8 8 0 1 1 16 0h-2a6 6 0 1 0-12 0H4zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 312 B |
@ -1,23 +0,0 @@
|
||||
const { execCommand } = require('@joplin/utils');
|
||||
const yargs = require('yargs');
|
||||
|
||||
async function main() {
|
||||
const argv = yargs.argv;
|
||||
if (!argv.profile) throw new Error('OneNote build: profile value is missing');
|
||||
if (!['release', 'dev'].includes(argv.profile)) throw new Error('OneNote build: profile value is invalid');
|
||||
|
||||
const buildCommand = `wasm-pack build --target nodejs --${argv.profile}`;
|
||||
|
||||
await execCommand(buildCommand);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error');
|
||||
if (error.stderr.includes('No such file or directory (os error 2)')) {
|
||||
console.error('----------------------------------------------------------------');
|
||||
console.error('Rust toolchain is missing, please install it: https://rustup.rs/');
|
||||
console.error('----------------------------------------------------------------');
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
@ -1,22 +0,0 @@
|
||||
[advisories]
|
||||
vulnerability = "deny"
|
||||
unmaintained = "warn"
|
||||
yanked = "warn"
|
||||
notice = "deny"
|
||||
|
||||
[licenses]
|
||||
unlicensed = "deny"
|
||||
allow-osi-fsf-free = "either"
|
||||
copyleft = "allow"
|
||||
default = "deny"
|
||||
|
||||
[bans]
|
||||
multiple-versions = "deny"
|
||||
wildcards = "warn"
|
||||
skip = [
|
||||
{ name = "cfg-if" },
|
||||
]
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
@ -1,49 +0,0 @@
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
function mkdirSyncRecursive(filepath) {
|
||||
if (!fs.existsSync(filepath)) {
|
||||
mkdirSyncRecursive(filepath.substring(0, filepath.lastIndexOf(path.sep)));
|
||||
fs.mkdirSync(filepath);
|
||||
}
|
||||
}
|
||||
|
||||
function isDirectory(filepath) {
|
||||
if (!fs.existsSync(filepath)) return false;
|
||||
return fs.lstatSync(filepath).isDirectory();
|
||||
}
|
||||
|
||||
function readDir(filepath) {
|
||||
const dirContents = fs.readdirSync(filepath, { withFileTypes: true });
|
||||
return dirContents.map(entry => filepath + path.sep + entry.name).join('\n');
|
||||
}
|
||||
|
||||
function removePrefix(basePath, prefix) {
|
||||
return basePath.replace(prefix, '');
|
||||
}
|
||||
|
||||
function getOutputPath(inputDir, outputDir, filePath) {
|
||||
const basePathFromInputFolder = filePath.replace(inputDir, '');
|
||||
const newOutput = path.join(outputDir, basePathFromInputFolder);
|
||||
return path.dirname(newOutput);
|
||||
}
|
||||
|
||||
function getParentDir(filePath) {
|
||||
return path.basename(path.dirname(filePath));
|
||||
}
|
||||
|
||||
function normalizeAndWriteFile(filePath, data) {
|
||||
filePath = path.normalize(filePath);
|
||||
fs.writeFileSync(filePath, data);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mkdirSyncRecursive,
|
||||
isDirectory,
|
||||
readDir,
|
||||
removePrefix,
|
||||
getOutputPath,
|
||||
getParentDir,
|
||||
normalizeAndWriteFile,
|
||||
};
|
@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "@joplin/onenote-converter",
|
||||
"collaborators": [
|
||||
"Pedro Luiz <pedrlz.frn@gmail.com>"
|
||||
],
|
||||
"description": "This package file only exists to build the @joplin/onenote-converter",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laurent22/joplin"
|
||||
},
|
||||
"files": [
|
||||
"./pkg/onenote_converter_bg.wasm",
|
||||
"./pkg/onenote_converter.js",
|
||||
"./pkg/onenote_converter.d.ts"
|
||||
],
|
||||
"main": "./pkg/onenote_converter.js",
|
||||
"types": "./pkg/onenote_converter.d.ts",
|
||||
"scripts": {
|
||||
"build": "node ./build.js --profile=release",
|
||||
"buildDev": "node ./build.js --profile=dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wasm-pack": "0.13.0",
|
||||
"yargs": "17.7.2"
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
pub use crate::parser::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use color_eyre::eyre::Result;
|
||||
use std::panic;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use crate::utils::utils::{log, log_warn};
|
||||
use crate::utils::{get_file_extension, get_file_name, get_output_path, get_parent_dir};
|
||||
|
||||
mod notebook;
|
||||
mod page;
|
||||
mod parser;
|
||||
mod section;
|
||||
mod templates;
|
||||
mod utils;
|
||||
|
||||
extern crate console_error_panic_hook;
|
||||
extern crate web_sys;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn oneNoteConverter(input: &str, output: &str, base_path: &str) {
|
||||
panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
|
||||
if let Err(e) = _main(input, output, base_path) {
|
||||
log_warn!("{:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn _main(input_path: &str, output_dir: &str, base_path: &str) -> Result<()> {
|
||||
log!("Starting parsing of the file: {:?}", input_path);
|
||||
convert(&input_path, &output_dir, base_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn convert(path: &str, output_dir: &str, base_path: &str) -> Result<()> {
|
||||
let mut parser = Parser::new();
|
||||
|
||||
let extension: String = unsafe { get_file_extension(path) }
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
|
||||
match extension.as_str() {
|
||||
".one" => {
|
||||
let _name: String = unsafe { get_file_name(path) }.unwrap().as_string().unwrap();
|
||||
log!("Parsing .one file: {}", _name);
|
||||
|
||||
if path.contains("OneNote_RecycleBin") {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let section = parser.parse_section(path.to_owned())?;
|
||||
|
||||
let section_output_dir = unsafe { get_output_path(base_path, output_dir, path) }
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
|
||||
section::Renderer::new().render(§ion, section_output_dir.to_owned())?;
|
||||
}
|
||||
".onetoc2" => {
|
||||
let _name: String = unsafe { get_file_name(path) }.unwrap().as_string().unwrap();
|
||||
log!("Parsing .onetoc2 file: {}", _name);
|
||||
|
||||
let notebook = parser.parse_notebook(path.to_owned())?;
|
||||
|
||||
let notebook_name = unsafe { get_parent_dir(path) }
|
||||
.expect("Input file has no parent folder")
|
||||
.as_string()
|
||||
.expect("Parent folder has no name");
|
||||
log!("notebook name: {:?}", notebook_name);
|
||||
|
||||
let notebook_output_dir = unsafe { get_output_path(base_path, output_dir, path) }
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
log!("Notebok directory: {:?}", notebook_output_dir);
|
||||
|
||||
notebook::Renderer::new().render(¬ebook, ¬ebook_name, ¬ebook_output_dir)?;
|
||||
}
|
||||
ext => return Err(eyre!("Invalid file extension: {}, file: {}", ext, path)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
use crate::parser::notebook::Notebook;
|
||||
use crate::parser::property::common::Color;
|
||||
use crate::parser::section::{Section, SectionEntry};
|
||||
use crate::templates::notebook::Toc;
|
||||
use crate::utils::utils::log;
|
||||
use crate::utils::{join_path, make_dir, remove_prefix};
|
||||
use crate::{section, templates};
|
||||
use color_eyre::eyre::Result;
|
||||
use palette::rgb::Rgb;
|
||||
use palette::{Alpha, ConvertFrom, Hsl, Saturate, Shade, Srgb};
|
||||
|
||||
pub(crate) type RgbColor = Alpha<Rgb<palette::encoding::Srgb, u8>, f32>;
|
||||
|
||||
pub(crate) struct Renderer;
|
||||
|
||||
impl Renderer {
|
||||
pub fn new() -> Self {
|
||||
Renderer
|
||||
}
|
||||
|
||||
pub fn render(&mut self, notebook: &Notebook, name: &str, output_dir: &str) -> Result<()> {
|
||||
log!("Notebook name: {:?} {:?}", name, output_dir);
|
||||
let _ = unsafe { make_dir(output_dir) };
|
||||
|
||||
// let notebook_dir = unsafe { join_path(output_dir, sanitize_filename::sanitize(name).as_str()) }.unwrap().as_string().unwrap();
|
||||
let notebook_dir = output_dir.to_owned();
|
||||
|
||||
let _ = unsafe { make_dir(¬ebook_dir) };
|
||||
|
||||
let mut toc = Vec::new();
|
||||
|
||||
for entry in notebook.entries() {
|
||||
match entry {
|
||||
SectionEntry::Section(section) => {
|
||||
toc.push(Toc::Section(self.render_section(
|
||||
section,
|
||||
notebook_dir.clone(),
|
||||
output_dir.into(),
|
||||
)?));
|
||||
}
|
||||
SectionEntry::SectionGroup(group) => {
|
||||
let dir_name = sanitize_filename::sanitize(group.display_name());
|
||||
let section_group_dir =
|
||||
unsafe { join_path(notebook_dir.as_str(), dir_name.as_str()) }
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
|
||||
log!("Section group directory: {:?}", section_group_dir);
|
||||
let _ = unsafe { make_dir(section_group_dir.as_str()) };
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for entry in group.entries() {
|
||||
if let SectionEntry::Section(section) = entry {
|
||||
entries.push(self.render_section(
|
||||
section,
|
||||
section_group_dir.clone(),
|
||||
output_dir.to_owned(),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
|
||||
toc.push(templates::notebook::Toc::SectionGroup(
|
||||
group.display_name().to_string(),
|
||||
entries,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
templates::notebook::render(name, &toc)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_section(
|
||||
&mut self,
|
||||
section: &Section,
|
||||
notebook_dir: String,
|
||||
base_dir: String,
|
||||
) -> Result<templates::notebook::Section> {
|
||||
let mut renderer = section::Renderer::new();
|
||||
let section_path = renderer.render(section, notebook_dir)?;
|
||||
log!("section_path: {:?}", section_path);
|
||||
|
||||
let path_from_base_dir = unsafe { remove_prefix(section_path.as_str(), base_dir.as_str()) }
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
log!("path_from_base_dir: {:?}", path_from_base_dir);
|
||||
Ok(templates::notebook::Section {
|
||||
name: section.display_name().to_string(),
|
||||
path: path_from_base_dir,
|
||||
color: section.color().map(prepare_color),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_color(color: Color) -> RgbColor {
|
||||
Alpha {
|
||||
alpha: color.alpha() as f32 / 255.0,
|
||||
color: Srgb::convert_from(
|
||||
Hsl::convert_from(Srgb::new(
|
||||
color.r() as f32 / 255.0,
|
||||
color.g() as f32 / 255.0,
|
||||
color.b() as f32 / 255.0,
|
||||
))
|
||||
.darken(0.2)
|
||||
.saturate(1.0),
|
||||
)
|
||||
.into_format(),
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
use crate::page::Renderer;
|
||||
use color_eyre::Result;
|
||||
use log::warn;
|
||||
// use crate::something_else::contents::Content;
|
||||
use crate::parser::contents::Content;
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub(crate) fn render_content(&mut self, content: &Content) -> Result<String> {
|
||||
match content {
|
||||
Content::RichText(text) => self.render_rich_text(text),
|
||||
Content::Image(image) => self.render_image(image),
|
||||
Content::EmbeddedFile(file) => self.render_embedded_file(file),
|
||||
Content::Table(table) => self.render_table(table),
|
||||
Content::Ink(ink) => Ok(self.render_ink(ink, None, false)),
|
||||
Content::Unknown => {
|
||||
warn!("Page with unknown content");
|
||||
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
use crate::page::Renderer;
|
||||
use crate::parser::contents::EmbeddedFile;
|
||||
use crate::parser::property::embedded_file::FileType;
|
||||
use crate::utils::utils::log;
|
||||
use crate::utils::{join_path, write_file};
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use std::path::PathBuf;
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub(crate) fn render_embedded_file(&mut self, file: &EmbeddedFile) -> Result<String> {
|
||||
let content;
|
||||
|
||||
let filename = self.determine_filename(file.filename())?;
|
||||
let path = unsafe { join_path(self.output.as_str(), filename.as_str()) }
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
log!("Rendering embedded file: {:?}", path);
|
||||
let _ = unsafe { write_file(path.as_str(), file.data()) };
|
||||
|
||||
let file_type = Self::guess_type(file);
|
||||
|
||||
match file_type {
|
||||
FileType::Audio => content = format!("<audio controls src=\"{}\"></audio>", filename),
|
||||
FileType::Video => content = format!("<video controls src=\"{}\"></video>", filename),
|
||||
FileType::Unknown => {
|
||||
content = format!(
|
||||
"<p style=\"font-size: 11pt; line-height: 17px;\"><a href=\"{}\">{}</a></p>",
|
||||
filename, filename
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(self.render_with_note_tags(file.note_tags(), content))
|
||||
}
|
||||
|
||||
fn guess_type(file: &EmbeddedFile) -> FileType {
|
||||
match file.file_type() {
|
||||
FileType::Audio => return FileType::Audio,
|
||||
FileType::Video => return FileType::Video,
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let filename = file.filename();
|
||||
|
||||
if let Some(mime) = mime_guess::from_path(filename).first() {
|
||||
if mime.type_() == "audio" {
|
||||
return FileType::Audio;
|
||||
}
|
||||
|
||||
if mime.type_() == "video" {
|
||||
return FileType::Video;
|
||||
}
|
||||
}
|
||||
FileType::Unknown
|
||||
}
|
||||
|
||||
pub(crate) fn determine_filename(&mut self, filename: &str) -> Result<String> {
|
||||
let mut i = 0;
|
||||
let mut current_filename = filename.to_string();
|
||||
|
||||
loop {
|
||||
if !self.section.files.contains(¤t_filename) {
|
||||
self.section.files.insert(current_filename.clone());
|
||||
|
||||
return Ok(current_filename);
|
||||
}
|
||||
|
||||
let path = PathBuf::from(filename);
|
||||
let ext = path
|
||||
.extension()
|
||||
.wrap_err("Embedded file has no extension")?
|
||||
.to_str()
|
||||
.wrap_err("Embedded file name is non utf-8")?;
|
||||
let base = path
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.wrap_err("Embedded file name is non utf-8")?
|
||||
.strip_suffix(ext)
|
||||
.wrap_err("Failed to strip extension from file name")?
|
||||
.trim_matches('.');
|
||||
|
||||
current_filename = format!("{}-{}.{}", base, i, ext);
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
use crate::page::Renderer;
|
||||
use crate::parser::contents::Image;
|
||||
use crate::utils::utils::log;
|
||||
use crate::utils::{join_path, px, write_file, AttributeSet, StyleSet};
|
||||
use color_eyre::Result;
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub(crate) fn render_image(&mut self, image: &Image) -> Result<String> {
|
||||
let mut content = String::new();
|
||||
|
||||
if let Some(data) = image.data() {
|
||||
let filename = self.determine_image_filename(image)?;
|
||||
let path = unsafe { join_path(self.output.as_str(), filename.as_str()) }
|
||||
.unwrap()
|
||||
.as_string()
|
||||
.unwrap();
|
||||
log!("Rendering image: {:?}", path);
|
||||
let _ = unsafe { write_file(path.as_str(), data) };
|
||||
|
||||
let mut attrs = AttributeSet::new();
|
||||
let mut styles = StyleSet::new();
|
||||
|
||||
attrs.set("src", filename);
|
||||
|
||||
if let Some(text) = image.alt_text() {
|
||||
attrs.set("alt", text.to_string().replace('"', """));
|
||||
}
|
||||
|
||||
if let Some(width) = image.layout_max_width() {
|
||||
styles.set("max-width", px(width));
|
||||
}
|
||||
|
||||
if let Some(height) = image.layout_max_height() {
|
||||
styles.set("max-height", px(height));
|
||||
}
|
||||
|
||||
if image.offset_horizontal().is_some() || image.offset_vertical().is_some() {
|
||||
styles.set("position", "absolute".to_string());
|
||||
}
|
||||
|
||||
if let Some(offset) = image.offset_horizontal() {
|
||||
styles.set("left", px(offset));
|
||||
}
|
||||
|
||||
if let Some(offset) = image.offset_vertical() {
|
||||
styles.set("top", px(offset));
|
||||
}
|
||||
|
||||
if styles.len() > 0 {
|
||||
attrs.set("style", styles.to_string());
|
||||
}
|
||||
|
||||
content.push_str(&format!("<img {} />", attrs.to_string()));
|
||||
}
|
||||
|
||||
Ok(self.render_with_note_tags(image.note_tags(), content))
|
||||
}
|
||||
|
||||
fn determine_image_filename(&mut self, image: &Image) -> Result<String> {
|
||||
if let Some(name) = image.image_filename() {
|
||||
return self.determine_filename(name);
|
||||
}
|
||||
|
||||
if let Some(ext) = image.extension() {
|
||||
let mut i = 0;
|
||||
|
||||
loop {
|
||||
let filename = format!("image{}{}", i, ext);
|
||||
|
||||
if !self.section.files.contains(&filename) {
|
||||
self.section.files.insert(filename.clone());
|
||||
|
||||
return Ok(filename);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
@ -1,210 +0,0 @@
|
||||
use crate::page::Renderer;
|
||||
use crate::parser::contents::{Ink, InkBoundingBox, InkPoint, InkStroke};
|
||||
use crate::utils::{px, AttributeSet, StyleSet};
|
||||
use itertools::Itertools;
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
const SVG_SCALING_FACTOR: f32 = 2540.0 / 96.0;
|
||||
|
||||
pub(crate) fn render_ink(
|
||||
&mut self,
|
||||
ink: &Ink,
|
||||
display_bounding_box: Option<&InkBoundingBox>,
|
||||
embedded: bool,
|
||||
) -> String {
|
||||
if ink.ink_strokes().is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut attrs = AttributeSet::new();
|
||||
let mut styles = StyleSet::new();
|
||||
|
||||
styles.set("overflow", "visible".to_string());
|
||||
styles.set("position", "absolute".to_string());
|
||||
|
||||
let path = self.render_ink_path(ink.ink_strokes());
|
||||
|
||||
let offset_horizontal = ink
|
||||
.offset_horizontal()
|
||||
.filter(|_| !embedded)
|
||||
.unwrap_or_default();
|
||||
let offset_vertical = ink
|
||||
.offset_vertical()
|
||||
.filter(|_| !embedded)
|
||||
.unwrap_or_default();
|
||||
|
||||
let display_bounding_box = ink
|
||||
.bounding_box()
|
||||
.or_else(|| display_bounding_box.map(|bb| bb.scale(Self::SVG_SCALING_FACTOR)))
|
||||
.filter(|_| embedded);
|
||||
|
||||
let (x_min, width) = get_boundary(ink.ink_strokes(), |p| p.x());
|
||||
let (y_min, height) = get_boundary(ink.ink_strokes(), |p| p.y());
|
||||
|
||||
let stroke_strength = ink.ink_strokes()[0]
|
||||
.width()
|
||||
.max(ink.ink_strokes()[0].height())
|
||||
.max(140.0);
|
||||
|
||||
let x_min = x_min as f32 - stroke_strength / 2.0;
|
||||
let y_min = y_min as f32 - stroke_strength / 2.0;
|
||||
|
||||
let width = width as f32 + stroke_strength + Self::SVG_SCALING_FACTOR;
|
||||
let height = height as f32 + stroke_strength + Self::SVG_SCALING_FACTOR;
|
||||
|
||||
styles.set(
|
||||
"height",
|
||||
format!(
|
||||
"{}px",
|
||||
((height as f32) / (Self::SVG_SCALING_FACTOR)).round()
|
||||
),
|
||||
);
|
||||
styles.set(
|
||||
"width",
|
||||
format!(
|
||||
"{}px",
|
||||
((width as f32) / (Self::SVG_SCALING_FACTOR)).round()
|
||||
),
|
||||
);
|
||||
|
||||
let display_y_min = display_bounding_box.map(|bb| bb.y()).unwrap_or_default();
|
||||
let display_x_min = display_bounding_box.map(|bb| bb.x()).unwrap_or_default();
|
||||
|
||||
styles.set(
|
||||
"top",
|
||||
format!(
|
||||
"{}px",
|
||||
((y_min - display_y_min) / Self::SVG_SCALING_FACTOR + offset_vertical * 48.0)
|
||||
.round()
|
||||
),
|
||||
);
|
||||
styles.set(
|
||||
"left",
|
||||
format!(
|
||||
"{}px",
|
||||
((x_min - display_x_min) / Self::SVG_SCALING_FACTOR + offset_horizontal * 48.0)
|
||||
.round()
|
||||
),
|
||||
);
|
||||
|
||||
attrs.set(
|
||||
"viewBox",
|
||||
format!(
|
||||
"{} {} {} {}",
|
||||
x_min.round(),
|
||||
y_min.round(),
|
||||
width.round(),
|
||||
height.round()
|
||||
),
|
||||
);
|
||||
|
||||
if styles.len() > 0 {
|
||||
attrs.set("style", styles.to_string());
|
||||
}
|
||||
|
||||
if embedded {
|
||||
let mut span_styles = StyleSet::new();
|
||||
|
||||
if let Some(bb) = display_bounding_box {
|
||||
span_styles.set("width", px(bb.width() / Self::SVG_SCALING_FACTOR / 48.0));
|
||||
span_styles.set("height", px(bb.height() / Self::SVG_SCALING_FACTOR / 48.0));
|
||||
}
|
||||
|
||||
format!(
|
||||
"<span style=\"{}\" class=\"ink-text\"><svg {}>{}</svg></span>",
|
||||
span_styles.to_string(),
|
||||
attrs.to_string(),
|
||||
path
|
||||
)
|
||||
} else {
|
||||
format!("<svg {}>{}</svg>", attrs.to_string(), path)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_ink_path(&mut self, strokes: &[InkStroke]) -> String {
|
||||
let mut attrs = AttributeSet::new();
|
||||
|
||||
attrs.set(
|
||||
"d",
|
||||
strokes
|
||||
.iter()
|
||||
.map(|stroke| self.render_ink_path_points(stroke))
|
||||
.collect_vec()
|
||||
.join(" "),
|
||||
);
|
||||
|
||||
let stroke = &strokes[0];
|
||||
|
||||
let opacity = (255 - stroke.transparency().unwrap_or_default()) as f32 / 256.0;
|
||||
attrs.set("opacity", format!("{:.2}", opacity));
|
||||
|
||||
let color = if let Some(value) = stroke.color() {
|
||||
let r = value % 256;
|
||||
|
||||
let rem = (value - r) / 256;
|
||||
let g = rem % 256;
|
||||
|
||||
let rem = (rem - g) / 256;
|
||||
let b = rem % 256;
|
||||
|
||||
format!("rgb({}, {}, {})", r, g, b)
|
||||
} else {
|
||||
"WindowText".to_string()
|
||||
};
|
||||
attrs.set("stroke", color);
|
||||
|
||||
attrs.set("stroke-width", stroke.width().round().to_string());
|
||||
|
||||
let pen_type = stroke.pen_tip().unwrap_or_default();
|
||||
attrs.set(
|
||||
"stroke-linejoin",
|
||||
if pen_type == 0 { "round" } else { "bevel" }.to_string(),
|
||||
);
|
||||
attrs.set(
|
||||
"stroke-linecap",
|
||||
if pen_type == 0 { "round" } else { "square" }.to_string(),
|
||||
);
|
||||
|
||||
attrs.set("fill", "none".to_string());
|
||||
|
||||
format!("<path {} />", attrs.to_string())
|
||||
}
|
||||
|
||||
fn render_ink_path_points(&self, stroke: &InkStroke) -> String {
|
||||
let start = &stroke.path()[0];
|
||||
let mut path = stroke.path()[1..].iter().map(display_point).collect_vec();
|
||||
|
||||
if path.is_empty() {
|
||||
path.push("0 0".to_string());
|
||||
}
|
||||
|
||||
format!("M {} l {}", display_point(start), path.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_boundary<F: Fn(&InkPoint) -> f32>(strokes: &[InkStroke], coord: F) -> (f32, f32) {
|
||||
let mut min = f32::INFINITY;
|
||||
let mut max = f32::NEG_INFINITY;
|
||||
|
||||
for stroke in strokes {
|
||||
let start = coord(&stroke.path()[0]);
|
||||
let mut pos = start;
|
||||
|
||||
for point in stroke.path()[1..].iter() {
|
||||
pos += coord(point);
|
||||
|
||||
if pos < min {
|
||||
min = pos;
|
||||
}
|
||||
if pos > max {
|
||||
max = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(min, max - min)
|
||||
}
|
||||
|
||||
fn display_point(p: &InkPoint) -> String {
|
||||
format!("{} {}", p.x().floor(), p.y().round())
|
||||
}
|
@ -1,182 +0,0 @@
|
||||
use crate::page::Renderer;
|
||||
use crate::parser::contents::{List, OutlineElement};
|
||||
use crate::parser::property::common::ColorRef;
|
||||
use crate::utils::{px, AttributeSet, StyleSet};
|
||||
use color_eyre::Result;
|
||||
|
||||
const FORMAT_NUMBERED_LIST: char = '\u{fffd}';
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub(crate) fn render_list<'b>(
|
||||
&mut self,
|
||||
elements: impl Iterator<Item = (&'b OutlineElement, u8, u8)>,
|
||||
indents: &[f32],
|
||||
) -> Result<String> {
|
||||
let mut contents = String::new();
|
||||
let mut in_list = false;
|
||||
let mut list_end = None;
|
||||
|
||||
for (element, parent_level, current_level) in elements {
|
||||
if !in_list && self.is_list(element) {
|
||||
let tags = self.list_tags(element);
|
||||
let list_start = tags.0;
|
||||
list_end = Some(tags.1);
|
||||
|
||||
contents.push_str(&list_start);
|
||||
in_list = true;
|
||||
}
|
||||
|
||||
if in_list && !self.is_list(element) {
|
||||
contents.push_str(&list_end.take().expect("no list end tag defined"));
|
||||
in_list = false;
|
||||
}
|
||||
|
||||
contents.push_str(&self.render_outline_element(
|
||||
element,
|
||||
parent_level,
|
||||
current_level,
|
||||
indents,
|
||||
)?);
|
||||
}
|
||||
|
||||
if in_list {
|
||||
contents.push_str(&list_end.expect("no list end tag defined"));
|
||||
}
|
||||
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
pub(crate) fn list_tags(&mut self, element: &OutlineElement) -> (String, String) {
|
||||
let list = element
|
||||
.list_contents()
|
||||
.first()
|
||||
.expect("no list contents defined");
|
||||
|
||||
let tag = if self.is_numbered_list(list) {
|
||||
"ol"
|
||||
} else {
|
||||
"ul"
|
||||
};
|
||||
let attrs = self.list_attrs(list, element.list_spacing());
|
||||
|
||||
(format!("<{} {}>", tag, attrs), format!("</{}>", tag))
|
||||
}
|
||||
|
||||
fn list_attrs(&mut self, list: &List, spacing: Option<f32>) -> AttributeSet {
|
||||
let mut attrs = AttributeSet::new();
|
||||
let mut container_style = StyleSet::new();
|
||||
let mut item_style = StyleSet::new();
|
||||
let mut marker_style = StyleSet::new();
|
||||
|
||||
let mut list_font = list.list_font();
|
||||
let mut list_format = list.list_format();
|
||||
let mut font_size = list.font_size();
|
||||
|
||||
self.fix_wingdings(&mut list_font, &mut list_format, &mut font_size);
|
||||
|
||||
match list_format {
|
||||
[FORMAT_NUMBERED_LIST, '\u{0}', ..] => {}
|
||||
[FORMAT_NUMBERED_LIST, '\u{1}', ..] => {
|
||||
container_style.set("list-style-type", "upper-roman".to_string())
|
||||
}
|
||||
[FORMAT_NUMBERED_LIST, '\u{2}', ..] => {
|
||||
container_style.set("list-style-type", "lower-roman".to_string())
|
||||
}
|
||||
[FORMAT_NUMBERED_LIST, '\u{3}', ..] => {
|
||||
container_style.set("list-style-type", "upper-latin".to_string())
|
||||
}
|
||||
[FORMAT_NUMBERED_LIST, '\u{4}', ..] => {
|
||||
container_style.set("list-style-type", "lower-latin".to_string())
|
||||
}
|
||||
[FORMAT_NUMBERED_LIST, c, ..] => {
|
||||
dbg!(c);
|
||||
unimplemented!();
|
||||
}
|
||||
[c] => marker_style.set("content", format!("'{}'", c)),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let bullet_spacing = spacing.unwrap_or(0.2);
|
||||
|
||||
item_style.set("padding-left", px(bullet_spacing));
|
||||
|
||||
container_style.set("position", "relative".to_string());
|
||||
container_style.set("left", px(-bullet_spacing));
|
||||
|
||||
if let Some(font) = list_font {
|
||||
marker_style.set("font-family", font.to_string());
|
||||
}
|
||||
|
||||
if let Some(font) = list.font() {
|
||||
marker_style.set("font-family", font.to_string());
|
||||
}
|
||||
|
||||
if let Some(ColorRef::Manual { r, g, b }) = list.font_color() {
|
||||
marker_style.set("color", format!("rgb({},{},{})", r, g, b));
|
||||
}
|
||||
|
||||
if let Some(size) = font_size {
|
||||
marker_style.set("font-size", ((size as f32) / 2.0).to_string() + "pt");
|
||||
}
|
||||
|
||||
if let Some(restart) = list.list_restart() {
|
||||
attrs.set("start", restart.to_string())
|
||||
}
|
||||
|
||||
if container_style.len() > 0 {
|
||||
attrs.set("style", container_style.to_string());
|
||||
}
|
||||
|
||||
let class = self.gen_class("list");
|
||||
|
||||
if marker_style.len() > 0 {
|
||||
attrs.set("class", class.clone());
|
||||
|
||||
self.global_styles
|
||||
.insert(format!(".{} li::marker", class), marker_style);
|
||||
}
|
||||
|
||||
self.global_styles
|
||||
.insert(format!(".{} li", class), item_style);
|
||||
|
||||
attrs
|
||||
}
|
||||
|
||||
fn fix_wingdings(
|
||||
&self,
|
||||
list_font: &mut Option<&str>,
|
||||
list_format: &mut &[char],
|
||||
font_size: &mut Option<u16>,
|
||||
) {
|
||||
match list_font.zip(list_format.first()) {
|
||||
// See http://www.alanwood.net/demos/wingdings.html
|
||||
Some(("Wingdings", '\u{a7}')) => *list_format = &['\u{25aa}'],
|
||||
Some(("Wingdings", '\u{a8}')) => *list_format = &['\u{25fb}'],
|
||||
Some(("Wingdings", '\u{77}')) => *list_format = &['\u{2b25}'],
|
||||
|
||||
// See http://www.alanwood.net/demos/wingdings-2.html
|
||||
Some(("Wingdings 2", '\u{ae}')) => *list_format = &['\u{25c6}'],
|
||||
|
||||
// See http://www.alanwood.net/demos/wingdings-3.html
|
||||
Some(("Wingdings 3", '\u{7d}')) => {
|
||||
*list_format = &['\u{25b6}'];
|
||||
*font_size = Some(18);
|
||||
}
|
||||
|
||||
_ => return,
|
||||
}
|
||||
|
||||
*list_font = Some("Calibri");
|
||||
}
|
||||
|
||||
fn is_numbered_list(&self, list: &List) -> bool {
|
||||
list.list_format()
|
||||
.first()
|
||||
.map(|c| *c == FORMAT_NUMBERED_LIST)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn is_list(&self, element: &OutlineElement) -> bool {
|
||||
element.list_contents().first().is_some()
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
use crate::parser::page::{Page, PageContent};
|
||||
use crate::section;
|
||||
use crate::utils::StyleSet;
|
||||
use color_eyre::Result;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub(crate) mod content;
|
||||
pub(crate) mod embedded_file;
|
||||
pub(crate) mod image;
|
||||
pub(crate) mod ink;
|
||||
pub(crate) mod list;
|
||||
pub(crate) mod note_tag;
|
||||
pub(crate) mod outline;
|
||||
pub(crate) mod rich_text;
|
||||
pub(crate) mod table;
|
||||
|
||||
pub(crate) struct Renderer<'a> {
|
||||
output: String,
|
||||
section: &'a mut section::Renderer,
|
||||
|
||||
in_list: bool,
|
||||
global_styles: HashMap<String, StyleSet>,
|
||||
global_classes: HashSet<String>,
|
||||
}
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub(crate) fn new(output: String, section: &'a mut section::Renderer) -> Self {
|
||||
Self {
|
||||
output,
|
||||
section,
|
||||
in_list: false,
|
||||
global_styles: HashMap::new(),
|
||||
global_classes: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn render_page(&mut self, page: &Page) -> Result<String> {
|
||||
let title_text = page.title_text().unwrap_or("Untitled Page");
|
||||
|
||||
let mut content = String::new();
|
||||
|
||||
if let Some(title) = page.title() {
|
||||
let mut styles = StyleSet::new();
|
||||
styles.set("position", "absolute".to_string());
|
||||
styles.set(
|
||||
"top",
|
||||
format!("{}px", (title.offset_vertical() * 48.0 + 24.0).round()),
|
||||
);
|
||||
styles.set(
|
||||
"left",
|
||||
format!("{}px", (title.offset_horizontal() * 48.0 + 48.0).round()),
|
||||
);
|
||||
|
||||
let mut title_field = format!("<div class=\"title\" style=\"{}\">", styles.to_string());
|
||||
|
||||
for outline in title.contents() {
|
||||
title_field.push_str(&self.render_outline(outline)?)
|
||||
}
|
||||
|
||||
title_field.push_str("</div>");
|
||||
|
||||
content.push_str(&title_field);
|
||||
}
|
||||
|
||||
let page_content = page
|
||||
.contents()
|
||||
.iter()
|
||||
.map(|content| self.render_page_content(content))
|
||||
.collect::<Result<String>>()?;
|
||||
|
||||
content.push_str(&page_content);
|
||||
|
||||
crate::templates::page::render(title_text, &content, &self.global_styles)
|
||||
}
|
||||
|
||||
pub(crate) fn gen_class(&mut self, prefix: &str) -> String {
|
||||
let mut i = 0;
|
||||
|
||||
loop {
|
||||
let class = format!("{}-{}", prefix, i);
|
||||
if !self.global_classes.contains(&class) {
|
||||
self.global_classes.insert(class.clone());
|
||||
|
||||
return class;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn render_page_content(&mut self, content: &PageContent) -> Result<String> {
|
||||
match content {
|
||||
PageContent::Outline(outline) => self.render_outline(outline),
|
||||
PageContent::Image(image) => self.render_image(image),
|
||||
PageContent::EmbeddedFile(file) => self.render_embedded_file(file),
|
||||
PageContent::Ink(ink) => Ok(self.render_ink(ink, None, false)),
|
||||
PageContent::Unknown => Ok(String::new()),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,539 +0,0 @@
|
||||
use crate::page::Renderer;
|
||||
use crate::parser::contents::{NoteTag, OutlineElement};
|
||||
use crate::parser::property::common::ColorRef;
|
||||
use crate::parser::property::note_tag::{ActionItemStatus, NoteTagShape};
|
||||
use crate::utils::StyleSet;
|
||||
use std::borrow::Cow;
|
||||
|
||||
const COLOR_BLUE: &str = "#4673b7";
|
||||
const COLOR_GREEN: &str = "#369950";
|
||||
const COLOR_ORANGE: &str = "#dba24d";
|
||||
const COLOR_PINK: &str = "#f78b9d";
|
||||
const COLOR_RED: &str = "#db5b4d";
|
||||
const COLOR_YELLOW: &str = "#ffd678";
|
||||
|
||||
const ICON_ARROW_RIGHT: &str = include_str!("../../assets/icons/arrow-right-line.svg");
|
||||
const ICON_AWARD: &str = include_str!("../../assets/icons/award-line.svg");
|
||||
const ICON_BOOK: &str = include_str!("../../assets/icons/book-open-line.svg");
|
||||
const ICON_BUBBLE: &str = include_str!("../../assets/icons/chat-4-line.svg");
|
||||
const ICON_CHECKBOX_COMPLETE: &str = include_str!("../../assets/icons/checkbox-fill.svg");
|
||||
const ICON_CHECKBOX_EMPTY: &str = include_str!("../../assets/icons/checkbox-blank-line.svg");
|
||||
const ICON_CHECK_MARK: &str = include_str!("../../assets/icons/check-line.svg");
|
||||
const ICON_CIRCLE: &str = include_str!("../../assets/icons/checkbox-blank-circle-fill.svg");
|
||||
const ICON_CONTACT: &str = include_str!("../../assets/icons/contacts-line.svg");
|
||||
const ICON_EMAIL: &str = include_str!("../../assets/icons/send-plane-2-line.svg");
|
||||
const ICON_ERROR: &str = include_str!("../../assets/icons/error-warning-line.svg");
|
||||
const ICON_FILM: &str = include_str!("../../assets/icons/film-line.svg");
|
||||
const ICON_FLAG: &str = include_str!("../../assets/icons/flag-fill.svg");
|
||||
const ICON_HOME: &str = include_str!("../../assets/icons/home-4-line.svg");
|
||||
const ICON_LIGHT_BULB: &str = include_str!("../../assets/icons/lightbulb-line.svg");
|
||||
const ICON_LINK: &str = include_str!("../../assets/icons/link.svg");
|
||||
const ICON_LOCK: &str = include_str!("../../assets/icons/lock-line.svg");
|
||||
const ICON_MUSIC: &str = include_str!("../../assets/icons/music-fill.svg");
|
||||
const ICON_PAPER: &str = include_str!("../../assets/icons/file-list-2-line.svg");
|
||||
const ICON_PEN: &str = include_str!("../../assets/icons/mark-pen-line.svg");
|
||||
const ICON_PERSON: &str = include_str!("../../assets/icons/user-line.svg");
|
||||
const ICON_PHONE: &str = include_str!("../../assets/icons/phone-line.svg");
|
||||
const ICON_QUESTION_MARK: &str = include_str!("../../assets/icons/question-mark.svg");
|
||||
const ICON_SQUARE: &str = include_str!("../../assets/icons/checkbox-blank-fill.svg");
|
||||
const ICON_STAR: &str = include_str!("../../assets/icons/star-fill.svg");
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
enum IconSize {
|
||||
Normal,
|
||||
Large,
|
||||
}
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub(crate) fn render_with_note_tags(
|
||||
&mut self,
|
||||
note_tags: &[NoteTag],
|
||||
content: String,
|
||||
) -> String {
|
||||
if let Some((markup, styles)) = self.render_note_tags(note_tags) {
|
||||
let mut contents = String::new();
|
||||
contents.push_str(&format!("<div style=\"{}\">{}", styles, markup));
|
||||
contents.push_str(&content);
|
||||
contents.push_str("</div>");
|
||||
|
||||
contents
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn render_note_tags(&mut self, note_tags: &[NoteTag]) -> Option<(String, StyleSet)> {
|
||||
let mut markup = String::new();
|
||||
let mut styles = StyleSet::new();
|
||||
|
||||
if note_tags.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for note_tag in note_tags {
|
||||
if let Some(def) = note_tag.definition() {
|
||||
if let Some(ColorRef::Manual { r, g, b }) = def.highlight_color() {
|
||||
styles.set("background-color", format!("rgb({},{},{})", r, g, b));
|
||||
}
|
||||
|
||||
if let Some(ColorRef::Manual { r, g, b }) = def.text_color() {
|
||||
styles.set("color", format!("rgb({},{},{})", r, g, b));
|
||||
}
|
||||
|
||||
if def.shape() != NoteTagShape::NoIcon {
|
||||
let (icon, icon_style) =
|
||||
self.note_tag_icon(def.shape(), note_tag.item_status());
|
||||
let mut icon_classes = vec!["note-tag-icon".to_string()];
|
||||
|
||||
if icon_style.len() > 0 {
|
||||
let class = self.gen_class("icon");
|
||||
icon_classes.push(class.to_string());
|
||||
|
||||
self.global_styles
|
||||
.insert(format!(".{} > svg", class), icon_style);
|
||||
}
|
||||
|
||||
markup.push_str(&format!(
|
||||
"<span class=\"{}\">{}</span>",
|
||||
icon_classes.join(" "),
|
||||
icon
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some((markup, styles))
|
||||
}
|
||||
|
||||
pub(crate) fn has_note_tag(&self, element: &OutlineElement) -> bool {
|
||||
element
|
||||
.contents()
|
||||
.iter()
|
||||
.flat_map(|element| element.rich_text())
|
||||
.any(|text| !text.note_tags().is_empty())
|
||||
}
|
||||
|
||||
fn note_tag_icon(
|
||||
&self,
|
||||
shape: NoteTagShape,
|
||||
status: ActionItemStatus,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
let mut style = StyleSet::new();
|
||||
|
||||
match shape {
|
||||
NoteTagShape::NoIcon => unimplemented!(),
|
||||
NoteTagShape::GreenCheckBox => self.icon_checkbox(status, style, COLOR_GREEN),
|
||||
NoteTagShape::YellowCheckBox => self.icon_checkbox(status, style, COLOR_YELLOW),
|
||||
NoteTagShape::BlueCheckBox => self.icon_checkbox(status, style, COLOR_BLUE),
|
||||
NoteTagShape::GreenStarCheckBox => {
|
||||
self.icon_checkbox_with_star(status, style, COLOR_GREEN)
|
||||
}
|
||||
NoteTagShape::YellowStarCheckBox => {
|
||||
self.icon_checkbox_with_star(status, style, COLOR_YELLOW)
|
||||
}
|
||||
NoteTagShape::BlueStarCheckBox => {
|
||||
self.icon_checkbox_with_star(status, style, COLOR_BLUE)
|
||||
}
|
||||
NoteTagShape::GreenExclamationCheckBox => {
|
||||
self.icon_checkbox_with_exclamation(status, style, COLOR_GREEN)
|
||||
}
|
||||
NoteTagShape::YellowExclamationCheckBox => {
|
||||
self.icon_checkbox_with_exclamation(status, style, COLOR_YELLOW)
|
||||
}
|
||||
NoteTagShape::BlueExclamationCheckBox => {
|
||||
self.icon_checkbox_with_exclamation(status, style, COLOR_BLUE)
|
||||
}
|
||||
NoteTagShape::GreenRightArrowCheckBox => {
|
||||
self.icon_checkbox_with_right_arrow(status, style, COLOR_GREEN)
|
||||
}
|
||||
NoteTagShape::YellowRightArrowCheckBox => {
|
||||
self.icon_checkbox_with_right_arrow(status, style, COLOR_YELLOW)
|
||||
}
|
||||
NoteTagShape::BlueRightArrowCheckBox => {
|
||||
self.icon_checkbox_with_right_arrow(status, style, COLOR_BLUE)
|
||||
}
|
||||
NoteTagShape::YellowStar => {
|
||||
style.set("fill", COLOR_YELLOW.to_string());
|
||||
|
||||
(
|
||||
Cow::from(ICON_STAR),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
)
|
||||
}
|
||||
NoteTagShape::BlueFollowUpFlag => unimplemented!(),
|
||||
NoteTagShape::QuestionMark => (
|
||||
Cow::from(ICON_QUESTION_MARK),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::BlueRightArrow => unimplemented!(),
|
||||
NoteTagShape::HighPriority => (
|
||||
Cow::from(ICON_ERROR),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::ContactInformation => (
|
||||
Cow::from(ICON_PHONE),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::Meeting => unimplemented!(),
|
||||
NoteTagShape::TimeSensitive => unimplemented!(),
|
||||
NoteTagShape::LightBulb => (
|
||||
Cow::from(ICON_LIGHT_BULB),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::Pushpin => unimplemented!(),
|
||||
NoteTagShape::Home => (
|
||||
Cow::from(ICON_HOME),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::CommentBubble => (
|
||||
Cow::from(ICON_BUBBLE),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::SmilingFace => unimplemented!(),
|
||||
NoteTagShape::AwardRibbon => (
|
||||
Cow::from(ICON_AWARD),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::YellowKey => unimplemented!(),
|
||||
NoteTagShape::BlueCheckBox1 => self.icon_checkbox_with_1(status, style, COLOR_BLUE),
|
||||
NoteTagShape::BlueCircle1 => unimplemented!(),
|
||||
NoteTagShape::BlueCheckBox2 => self.icon_checkbox_with_2(status, style, COLOR_BLUE),
|
||||
NoteTagShape::BlueCircle2 => unimplemented!(),
|
||||
NoteTagShape::BlueCheckBox3 => self.icon_checkbox_with_3(status, style, COLOR_BLUE),
|
||||
NoteTagShape::BlueCircle3 => unimplemented!(),
|
||||
NoteTagShape::BlueEightPointStar => unimplemented!(),
|
||||
NoteTagShape::BlueCheckMark => self.icon_checkmark(style, COLOR_BLUE),
|
||||
NoteTagShape::BlueCircle => self.icon_circle(style, COLOR_BLUE),
|
||||
NoteTagShape::BlueDownArrow => unimplemented!(),
|
||||
NoteTagShape::BlueLeftArrow => unimplemented!(),
|
||||
NoteTagShape::BlueSolidTarget => unimplemented!(),
|
||||
NoteTagShape::BlueStar => unimplemented!(),
|
||||
NoteTagShape::BlueSun => unimplemented!(),
|
||||
NoteTagShape::BlueTarget => unimplemented!(),
|
||||
NoteTagShape::BlueTriangle => unimplemented!(),
|
||||
NoteTagShape::BlueUmbrella => unimplemented!(),
|
||||
NoteTagShape::BlueUpArrow => unimplemented!(),
|
||||
NoteTagShape::BlueXWithDots => unimplemented!(),
|
||||
NoteTagShape::BlueX => unimplemented!(),
|
||||
NoteTagShape::GreenCheckBox1 => self.icon_checkbox_with_1(status, style, COLOR_GREEN),
|
||||
NoteTagShape::GreenCircle1 => unimplemented!(),
|
||||
NoteTagShape::GreenCheckBox2 => self.icon_checkbox_with_2(status, style, COLOR_GREEN),
|
||||
NoteTagShape::GreenCircle2 => unimplemented!(),
|
||||
NoteTagShape::GreenCheckBox3 => self.icon_checkbox_with_3(status, style, COLOR_GREEN),
|
||||
NoteTagShape::GreenCircle3 => unimplemented!(),
|
||||
NoteTagShape::GreenEightPointStar => unimplemented!(),
|
||||
NoteTagShape::GreenCheckMark => self.icon_checkmark(style, COLOR_GREEN),
|
||||
NoteTagShape::GreenCircle => self.icon_circle(style, COLOR_GREEN),
|
||||
NoteTagShape::GreenDownArrow => unimplemented!(),
|
||||
NoteTagShape::GreenLeftArrow => unimplemented!(),
|
||||
NoteTagShape::GreenRightArrow => unimplemented!(),
|
||||
NoteTagShape::GreenSolidArrow => unimplemented!(),
|
||||
NoteTagShape::GreenStar => unimplemented!(),
|
||||
NoteTagShape::GreenSun => unimplemented!(),
|
||||
NoteTagShape::GreenTarget => unimplemented!(),
|
||||
NoteTagShape::GreenTriangle => unimplemented!(),
|
||||
NoteTagShape::GreenUmbrella => unimplemented!(),
|
||||
NoteTagShape::GreenUpArrow => unimplemented!(),
|
||||
NoteTagShape::GreenXWithDots => unimplemented!(),
|
||||
NoteTagShape::GreenX => unimplemented!(),
|
||||
NoteTagShape::YellowCheckBox1 => self.icon_checkbox_with_1(status, style, COLOR_YELLOW),
|
||||
NoteTagShape::YellowCircle1 => unimplemented!(),
|
||||
NoteTagShape::YellowCheckBox2 => self.icon_checkbox_with_2(status, style, COLOR_YELLOW),
|
||||
NoteTagShape::YellowCircle2 => unimplemented!(),
|
||||
NoteTagShape::YellowCheckBox3 => self.icon_checkbox_with_3(status, style, COLOR_YELLOW),
|
||||
NoteTagShape::YellowCircle3 => unimplemented!(),
|
||||
NoteTagShape::YellowEightPointStar => unimplemented!(),
|
||||
NoteTagShape::YellowCheckMark => self.icon_checkmark(style, COLOR_YELLOW),
|
||||
NoteTagShape::YellowCircle => self.icon_circle(style, COLOR_YELLOW),
|
||||
NoteTagShape::YellowDownArrow => unimplemented!(),
|
||||
NoteTagShape::YellowLeftArrow => unimplemented!(),
|
||||
NoteTagShape::YellowRightArrow => unimplemented!(),
|
||||
NoteTagShape::YellowSolidTarget => unimplemented!(),
|
||||
NoteTagShape::YellowSun => unimplemented!(),
|
||||
NoteTagShape::YellowTarget => unimplemented!(),
|
||||
NoteTagShape::YellowTriangle => unimplemented!(),
|
||||
NoteTagShape::YellowUmbrella => unimplemented!(),
|
||||
NoteTagShape::YellowUpArrow => unimplemented!(),
|
||||
NoteTagShape::YellowXWithDots => unimplemented!(),
|
||||
NoteTagShape::YellowX => unimplemented!(),
|
||||
NoteTagShape::FollowUpTodayFlag => unimplemented!(),
|
||||
NoteTagShape::FollowUpTomorrowFlag => unimplemented!(),
|
||||
NoteTagShape::FollowUpThisWeekFlag => unimplemented!(),
|
||||
NoteTagShape::FollowUpNextWeekFlag => unimplemented!(),
|
||||
NoteTagShape::NoFollowUpDateFlag => unimplemented!(),
|
||||
NoteTagShape::BluePersonCheckBox => {
|
||||
self.icon_checkbox_with_person(status, style, COLOR_BLUE)
|
||||
}
|
||||
NoteTagShape::YellowPersonCheckBox => {
|
||||
self.icon_checkbox_with_person(status, style, COLOR_YELLOW)
|
||||
}
|
||||
NoteTagShape::GreenPersonCheckBox => {
|
||||
self.icon_checkbox_with_person(status, style, COLOR_GREEN)
|
||||
}
|
||||
NoteTagShape::BlueFlagCheckBox => {
|
||||
self.icon_checkbox_with_flag(status, style, COLOR_BLUE)
|
||||
}
|
||||
NoteTagShape::RedFlagCheckBox => self.icon_checkbox_with_flag(status, style, COLOR_RED),
|
||||
NoteTagShape::GreenFlagCheckBox => {
|
||||
self.icon_checkbox_with_flag(status, style, COLOR_GREEN)
|
||||
}
|
||||
NoteTagShape::RedSquare => self.icon_square(style, COLOR_RED),
|
||||
NoteTagShape::YellowSquare => self.icon_square(style, COLOR_YELLOW),
|
||||
NoteTagShape::BlueSquare => self.icon_square(style, COLOR_BLUE),
|
||||
NoteTagShape::GreenSquare => self.icon_square(style, COLOR_GREEN),
|
||||
NoteTagShape::OrangeSquare => self.icon_square(style, COLOR_ORANGE),
|
||||
NoteTagShape::PinkSquare => self.icon_square(style, COLOR_PINK),
|
||||
NoteTagShape::EMailMessage => (
|
||||
Cow::from(ICON_EMAIL),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::ClosedEnvelope => unimplemented!(),
|
||||
NoteTagShape::OpenEnvelope => unimplemented!(),
|
||||
NoteTagShape::MobilePhone => unimplemented!(),
|
||||
NoteTagShape::TelephoneWithClock => unimplemented!(),
|
||||
NoteTagShape::QuestionBalloon => unimplemented!(),
|
||||
NoteTagShape::PaperClip => unimplemented!(),
|
||||
NoteTagShape::FrowningFace => unimplemented!(),
|
||||
NoteTagShape::InstantMessagingContactPerson => unimplemented!(),
|
||||
NoteTagShape::PersonWithExclamationMark => unimplemented!(),
|
||||
NoteTagShape::TwoPeople => unimplemented!(),
|
||||
NoteTagShape::ReminderBell => unimplemented!(),
|
||||
NoteTagShape::Contact => (
|
||||
Cow::from(ICON_CONTACT),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::RoseOnAStem => unimplemented!(),
|
||||
NoteTagShape::CalendarDateWithClock => unimplemented!(),
|
||||
NoteTagShape::MusicalNote => (
|
||||
Cow::from(ICON_MUSIC),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::MovieClip => (
|
||||
Cow::from(ICON_FILM),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::QuotationMark => unimplemented!(),
|
||||
NoteTagShape::Globe => unimplemented!(),
|
||||
NoteTagShape::HyperlinkGlobe => (
|
||||
Cow::from(ICON_LINK),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::Laptop => unimplemented!(),
|
||||
NoteTagShape::Plane => unimplemented!(),
|
||||
NoteTagShape::Car => unimplemented!(),
|
||||
NoteTagShape::Binoculars => unimplemented!(),
|
||||
NoteTagShape::PresentationSlide => unimplemented!(),
|
||||
NoteTagShape::Padlock => (
|
||||
Cow::from(ICON_LOCK),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::OpenBook => (
|
||||
Cow::from(ICON_BOOK),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::NotebookWithClock => unimplemented!(),
|
||||
NoteTagShape::BlankPaperWithLines => (
|
||||
Cow::from(ICON_PAPER),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::Research => unimplemented!(),
|
||||
NoteTagShape::Pen => (
|
||||
Cow::from(ICON_PEN),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
),
|
||||
NoteTagShape::DollarSign => unimplemented!(),
|
||||
NoteTagShape::CoinsWithAWindowBackdrop => unimplemented!(),
|
||||
NoteTagShape::ScheduledTask => unimplemented!(),
|
||||
NoteTagShape::LightningBolt => unimplemented!(),
|
||||
NoteTagShape::Cloud => unimplemented!(),
|
||||
NoteTagShape::Heart => unimplemented!(),
|
||||
NoteTagShape::Sunflower => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_checkbox(
|
||||
&self,
|
||||
status: ActionItemStatus,
|
||||
mut style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
style.set("fill", color.to_string());
|
||||
|
||||
if status.completed() {
|
||||
(
|
||||
Cow::from(ICON_CHECKBOX_COMPLETE),
|
||||
self.icon_style(IconSize::Large, style),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Cow::from(ICON_CHECKBOX_EMPTY),
|
||||
self.icon_style(IconSize::Large, style),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_checkbox_with_person(
|
||||
&self,
|
||||
status: ActionItemStatus,
|
||||
style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
self.icon_checkbox_with(status, style, color, ICON_PERSON)
|
||||
}
|
||||
|
||||
fn icon_checkbox_with_right_arrow(
|
||||
&self,
|
||||
status: ActionItemStatus,
|
||||
style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
self.icon_checkbox_with(status, style, color, ICON_ARROW_RIGHT)
|
||||
}
|
||||
|
||||
fn icon_checkbox_with_star(
|
||||
&self,
|
||||
status: ActionItemStatus,
|
||||
style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
self.icon_checkbox_with(status, style, color, ICON_STAR)
|
||||
}
|
||||
|
||||
fn icon_checkbox_with_flag(
|
||||
&self,
|
||||
status: ActionItemStatus,
|
||||
style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
self.icon_checkbox_with(status, style, color, ICON_FLAG)
|
||||
}
|
||||
|
||||
fn icon_checkbox_with_1(
|
||||
&self,
|
||||
status: ActionItemStatus,
|
||||
style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
self.icon_checkbox_with(status, style, color, "<span class=\"content\">1</span>")
|
||||
}
|
||||
|
||||
fn icon_checkbox_with_2(
|
||||
&self,
|
||||
status: ActionItemStatus,
|
||||
style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
self.icon_checkbox_with(status, style, color, "<span class=\"content\">2</span>")
|
||||
}
|
||||
|
||||
fn icon_checkbox_with_3(
|
||||
&self,
|
||||
status: ActionItemStatus,
|
||||
style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
self.icon_checkbox_with(status, style, color, "<span class=\"content\">3</span>")
|
||||
}
|
||||
|
||||
fn icon_checkbox_with_exclamation(
|
||||
&self,
|
||||
status: ActionItemStatus,
|
||||
style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
self.icon_checkbox_with(status, style, color, "<span class=\"content\">!</span>")
|
||||
}
|
||||
|
||||
fn icon_checkbox_with(
|
||||
&self,
|
||||
status: ActionItemStatus,
|
||||
mut style: StyleSet,
|
||||
color: &'static str,
|
||||
secondary_icon: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
style.set("fill", color.to_string());
|
||||
|
||||
let mut content = String::new();
|
||||
content.push_str(if status.completed() {
|
||||
ICON_CHECKBOX_COMPLETE
|
||||
} else {
|
||||
ICON_CHECKBOX_EMPTY
|
||||
});
|
||||
|
||||
content.push_str(&format!(
|
||||
"<span class=\"icon-secondary\">{}</span>",
|
||||
secondary_icon
|
||||
));
|
||||
|
||||
(Cow::from(content), self.icon_style(IconSize::Large, style))
|
||||
}
|
||||
|
||||
fn icon_checkmark(
|
||||
&self,
|
||||
mut style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
style.set("fill", color.to_string());
|
||||
|
||||
(
|
||||
Cow::from(ICON_CHECK_MARK),
|
||||
self.icon_style(IconSize::Large, style),
|
||||
)
|
||||
}
|
||||
|
||||
fn icon_circle(
|
||||
&self,
|
||||
mut style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
style.set("fill", color.to_string());
|
||||
|
||||
(
|
||||
Cow::from(ICON_CIRCLE),
|
||||
self.icon_style(IconSize::Normal, style),
|
||||
)
|
||||
}
|
||||
|
||||
fn icon_square(
|
||||
&self,
|
||||
mut style: StyleSet,
|
||||
color: &'static str,
|
||||
) -> (Cow<'static, str>, StyleSet) {
|
||||
style.set("fill", color.to_string());
|
||||
|
||||
(
|
||||
Cow::from(ICON_SQUARE),
|
||||
self.icon_style(IconSize::Large, style),
|
||||
)
|
||||
}
|
||||
|
||||
fn icon_style(&self, size: IconSize, mut style: StyleSet) -> StyleSet {
|
||||
match size {
|
||||
IconSize::Normal => {
|
||||
style.set("height", "16px".to_string());
|
||||
style.set("width", "16px".to_string());
|
||||
}
|
||||
IconSize::Large => {
|
||||
style.set("height", "20px".to_string());
|
||||
style.set("width", "20px".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
match (self.in_list, size) {
|
||||
(false, IconSize::Normal) => {
|
||||
style.set("left", "-23px".to_string());
|
||||
}
|
||||
(false, IconSize::Large) => {
|
||||
style.set("left", "-25px".to_string());
|
||||
}
|
||||
(true, IconSize::Normal) => {
|
||||
style.set("left", "-38px".to_string());
|
||||
}
|
||||
(true, IconSize::Large) => {
|
||||
style.set("left", "-40px".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
style
|
||||
}
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
use crate::page::Renderer;
|
||||
use crate::parser::contents::{Outline, OutlineElement, OutlineItem};
|
||||
use crate::utils::{px, AttributeSet, StyleSet};
|
||||
use color_eyre::Result;
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub(crate) fn render_outline(&mut self, outline: &Outline) -> Result<String> {
|
||||
let mut attrs = AttributeSet::new();
|
||||
let mut styles = StyleSet::new();
|
||||
let mut contents = String::new();
|
||||
|
||||
attrs.set("class", "container-outline".to_string());
|
||||
|
||||
if let Some(width) = outline.layout_max_width() {
|
||||
let outline_width = if outline.is_layout_size_set_by_user() {
|
||||
width
|
||||
} else {
|
||||
width.max(13.0)
|
||||
};
|
||||
|
||||
styles.set("width", px(outline_width));
|
||||
};
|
||||
|
||||
if outline.offset_horizontal().is_some() || outline.offset_vertical().is_some() {
|
||||
styles.set("position", "absolute".to_string());
|
||||
}
|
||||
|
||||
if let Some(offset) = outline.offset_horizontal() {
|
||||
styles.set("left", px(offset));
|
||||
}
|
||||
|
||||
if let Some(offset) = outline.offset_vertical() {
|
||||
styles.set("top", px(offset));
|
||||
}
|
||||
|
||||
if styles.len() > 0 {
|
||||
attrs.set("style", styles.to_string());
|
||||
}
|
||||
|
||||
contents.push_str(&format!("<div {}>", attrs));
|
||||
contents.push_str(&self.render_outline_items(
|
||||
outline.items(),
|
||||
0,
|
||||
outline.child_level(),
|
||||
outline.indents(),
|
||||
)?);
|
||||
contents.push_str("</div>");
|
||||
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
pub(crate) fn render_outline_items(
|
||||
&mut self,
|
||||
items: &[OutlineItem],
|
||||
parent_level: u8,
|
||||
current_level: u8,
|
||||
indents: &[f32],
|
||||
) -> Result<String> {
|
||||
self.render_list(
|
||||
flatten_outline_items(items, parent_level, current_level),
|
||||
indents,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn render_outline_element(
|
||||
&mut self,
|
||||
element: &OutlineElement,
|
||||
parent_level: u8,
|
||||
current_level: u8,
|
||||
indents: &[f32],
|
||||
) -> Result<String> {
|
||||
let mut indent_width = 0.0;
|
||||
for i in (parent_level + 1)..=current_level {
|
||||
indent_width += indents.get(i as usize).copied().unwrap_or(0.75);
|
||||
}
|
||||
|
||||
let mut contents = String::new();
|
||||
let is_list = self.is_list(element);
|
||||
|
||||
let mut attrs = AttributeSet::new();
|
||||
attrs.set("class", "outline-element".to_string());
|
||||
|
||||
let mut styles = StyleSet::new();
|
||||
styles.set("margin-left", px(indent_width as f32));
|
||||
attrs.set("style", styles.to_string());
|
||||
|
||||
if is_list {
|
||||
contents.push_str(&format!("<li {}>", attrs));
|
||||
} else {
|
||||
contents.push_str(&format!("<div {}>", attrs));
|
||||
}
|
||||
|
||||
self.in_list = is_list;
|
||||
|
||||
contents.extend(
|
||||
element
|
||||
.contents()
|
||||
.iter()
|
||||
.map(|content| self.render_content(content))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
self.in_list = false;
|
||||
|
||||
if !is_list {
|
||||
contents.push_str("</div>");
|
||||
}
|
||||
|
||||
let children = element.children();
|
||||
|
||||
if !children.is_empty() {
|
||||
contents.push_str(&self.render_outline_items(
|
||||
children,
|
||||
current_level,
|
||||
current_level + element.child_level(),
|
||||
indents,
|
||||
)?);
|
||||
}
|
||||
|
||||
if is_list {
|
||||
contents.push_str("</li>");
|
||||
}
|
||||
|
||||
contents.push('\n');
|
||||
|
||||
Ok(contents)
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_outline_items<'a>(
|
||||
items: &'a [OutlineItem],
|
||||
parent_level: u8,
|
||||
current_level: u8,
|
||||
) -> Box<dyn Iterator<Item = (&'a OutlineElement, u8, u8)> + 'a> {
|
||||
Box::new(items.iter().flat_map(move |item| match item {
|
||||
OutlineItem::Element(element) => {
|
||||
Box::new(Some((element, parent_level, current_level)).into_iter())
|
||||
}
|
||||
OutlineItem::Group(group) => flatten_outline_items(
|
||||
group.outlines(),
|
||||
parent_level,
|
||||
current_level + group.child_level(),
|
||||
),
|
||||
}))
|
||||
}
|
@ -1,306 +0,0 @@
|
||||
use crate::page::Renderer;
|
||||
use crate::parser::contents::{EmbeddedObject, RichText};
|
||||
use crate::parser::property::common::ColorRef;
|
||||
use crate::parser::property::rich_text::{ParagraphAlignment, ParagraphStyling};
|
||||
use crate::utils::{px, AttributeSet, StyleSet};
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::Result;
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Captures, Regex};
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub(crate) fn render_rich_text(&mut self, text: &RichText) -> Result<String> {
|
||||
let mut content = String::new();
|
||||
let mut attrs = AttributeSet::new();
|
||||
let mut style = self.parse_paragraph_styles(text);
|
||||
|
||||
if let Some((note_tag_html, note_tag_styles)) = self.render_note_tags(text.note_tags()) {
|
||||
content.push_str(¬e_tag_html);
|
||||
style.extend(note_tag_styles);
|
||||
}
|
||||
|
||||
content.push_str(&self.parse_content(text)?);
|
||||
|
||||
if content.starts_with("http://") || content.starts_with("https://") {
|
||||
content = format!("<a href=\"{}\">{}</a>", content, content);
|
||||
}
|
||||
|
||||
if style.len() > 0 {
|
||||
attrs.set("style", style.to_string());
|
||||
}
|
||||
|
||||
match text.paragraph_style().style_id() {
|
||||
Some(t) if !self.in_list && is_tag(t) => {
|
||||
Ok(format!("<{} {}>{}</{}>", t, attrs, content, t))
|
||||
}
|
||||
_ if style.len() > 0 => Ok(format!("<span style=\"{}\">{}</span>", style, content)),
|
||||
_ => Ok(content),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_content(&mut self, data: &RichText) -> Result<String> {
|
||||
if !data.embedded_objects().is_empty() {
|
||||
return Ok(data
|
||||
.embedded_objects()
|
||||
.iter()
|
||||
.map(|object| match object {
|
||||
EmbeddedObject::Ink(container) => {
|
||||
self.render_ink(container.ink(), container.bounding_box(), true)
|
||||
}
|
||||
EmbeddedObject::InkSpace(space) => {
|
||||
format!("<span class=\"ink-space\" style=\"padding-left: {}; padding-top: {};\"></span>",
|
||||
px(space.width()), px(space.height()))
|
||||
}
|
||||
EmbeddedObject::InkLineBreak => {
|
||||
"<span class=\"ink-linebreak\"><br></span>".to_string()
|
||||
}
|
||||
})
|
||||
.collect_vec()
|
||||
.join(""));
|
||||
}
|
||||
|
||||
let mut indices = data.text_run_indices().to_vec();
|
||||
let mut styles = data.text_run_formatting().to_vec();
|
||||
|
||||
let mut text = data.text().to_string();
|
||||
|
||||
if text.is_empty() {
|
||||
text = " ".to_string();
|
||||
}
|
||||
|
||||
// TODO: Maybe this shouldn't be here
|
||||
// When the this character is at the start of the paragraph it makes
|
||||
// all the styles to be shifted by minus one.
|
||||
// A better solution would be to look if there isn't anything wrong with the parser,
|
||||
// but I haven't found what could be causing this yet.
|
||||
if text.starts_with("\u{000B}") && !indices.is_empty(){
|
||||
indices.remove(0);
|
||||
styles.pop();
|
||||
}
|
||||
|
||||
if indices.is_empty() {
|
||||
return Ok(fix_newlines(&text));
|
||||
}
|
||||
|
||||
assert!(indices.len() + 1 >= styles.len());
|
||||
|
||||
// Split text into parts specified by indices
|
||||
let mut parts: Vec<String> = vec![];
|
||||
|
||||
for i in indices.iter().copied().rev() {
|
||||
let part = text.chars().skip(i as usize).collect();
|
||||
text = text.chars().take(i as usize).collect();
|
||||
|
||||
parts.push(part);
|
||||
}
|
||||
|
||||
if !indices.is_empty() {
|
||||
parts.push(text);
|
||||
}
|
||||
|
||||
let mut in_hyperlink = false;
|
||||
|
||||
let content = parts
|
||||
.into_iter()
|
||||
.rev()
|
||||
.zip(styles.iter())
|
||||
.map(|(text, style)| {
|
||||
if style.hyperlink() {
|
||||
let text = self.render_hyperlink(text, style, in_hyperlink);
|
||||
in_hyperlink = true;
|
||||
|
||||
text
|
||||
} else {
|
||||
in_hyperlink = false;
|
||||
|
||||
let style = self.parse_style(style);
|
||||
|
||||
if style.len() > 0 {
|
||||
Ok(format!("<span style=\"{}\">{}</span>", style, text))
|
||||
} else {
|
||||
Ok(text)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Result<String>>()?;
|
||||
|
||||
Ok(fix_newlines(&content))
|
||||
}
|
||||
|
||||
fn render_hyperlink(
|
||||
&self,
|
||||
text: String,
|
||||
style: &ParagraphStyling,
|
||||
in_hyperlink: bool,
|
||||
) -> Result<String> {
|
||||
const HYPERLINK_MARKER: &str = "\u{fddf}HYPERLINK \"";
|
||||
|
||||
let style = self.parse_style(style);
|
||||
|
||||
if text.starts_with(HYPERLINK_MARKER) {
|
||||
let url = text
|
||||
.strip_prefix(HYPERLINK_MARKER)
|
||||
.wrap_err("Hyperlink has no start marker")?
|
||||
.strip_suffix('"')
|
||||
.wrap_err("Hyperlink has no end marker")?;
|
||||
|
||||
Ok(format!("<a href=\"{}\" style=\"{}\">", url, style))
|
||||
} else if in_hyperlink {
|
||||
Ok(text + "</a>")
|
||||
} else {
|
||||
Ok(format!(
|
||||
"<a href=\"{}\" style=\"{}\">{}</a>",
|
||||
text, style, text
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_paragraph_styles(&self, text: &RichText) -> StyleSet {
|
||||
if !text.embedded_objects().is_empty() {
|
||||
assert_eq!(
|
||||
text.text(),
|
||||
"",
|
||||
"paragraph with text and embedded objects is not supported"
|
||||
);
|
||||
|
||||
return StyleSet::new();
|
||||
}
|
||||
|
||||
let mut styles = self.parse_style(text.paragraph_style());
|
||||
|
||||
if let [style] = text.text_run_formatting() {
|
||||
styles.extend(self.parse_style(style))
|
||||
}
|
||||
|
||||
if text.paragraph_space_before() > 0.0 {
|
||||
styles.set("padding-top", px(text.paragraph_space_before()))
|
||||
}
|
||||
|
||||
if text.paragraph_space_after() > 0.0 {
|
||||
styles.set("padding-bottom", px(text.paragraph_space_after()))
|
||||
}
|
||||
|
||||
if let Some(line_spacing) = text.paragraph_line_spacing_exact() {
|
||||
styles.set(
|
||||
"line-height",
|
||||
((line_spacing as f32) * 50.0).floor().to_string() + "pt",
|
||||
);
|
||||
// TODO: why not implemented?
|
||||
// if line_spacing > 0.0 {
|
||||
// dbg!(text);
|
||||
// unimplemented!();
|
||||
// }
|
||||
}
|
||||
|
||||
match text.paragraph_alignment() {
|
||||
ParagraphAlignment::Center => styles.set("text-align", "center".to_string()),
|
||||
ParagraphAlignment::Right => styles.set("text-align", "right".to_string()),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
styles
|
||||
}
|
||||
|
||||
fn parse_style(&self, style: &ParagraphStyling) -> StyleSet {
|
||||
let mut styles = StyleSet::new();
|
||||
|
||||
if style.bold() {
|
||||
styles.set("font-weight", "bold".to_string());
|
||||
}
|
||||
|
||||
if style.italic() {
|
||||
styles.set("font-style", "italic".to_string());
|
||||
}
|
||||
|
||||
if style.underline() {
|
||||
styles.set("text-decoration", "underline".to_string());
|
||||
}
|
||||
|
||||
if style.superscript() {
|
||||
styles.set("vertical-align", "super".to_string());
|
||||
}
|
||||
|
||||
if style.subscript() {
|
||||
styles.set("vertical-align", "sub".to_string());
|
||||
}
|
||||
|
||||
if style.strikethrough() {
|
||||
styles.set("text-decoration", "line-through".to_string());
|
||||
}
|
||||
|
||||
if let Some(font) = style.font() {
|
||||
styles.set("font-family", font.to_string());
|
||||
}
|
||||
|
||||
if let Some(size) = style.font_size() {
|
||||
styles.set("font-size", ((size as f32) / 2.0).to_string() + "pt");
|
||||
}
|
||||
|
||||
if let Some(ColorRef::Manual { r, g, b }) = style.font_color() {
|
||||
styles.set("color", format!("rgb({},{},{})", r, g, b));
|
||||
}
|
||||
|
||||
if let Some(ColorRef::Manual { r, g, b }) = style.highlight() {
|
||||
styles.set("background-color", format!("rgb({},{},{})", r, g, b));
|
||||
}
|
||||
|
||||
if style.paragraph_alignment().is_some() {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
if let Some(space) = style.paragraph_space_before() {
|
||||
if space != 0.0 {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(space) = style.paragraph_space_after() {
|
||||
if space != 0.0 {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(space) = style.paragraph_line_spacing_exact() {
|
||||
if space != 0.0 {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
if let Some(size) = style.font_size() {
|
||||
styles.set(
|
||||
"line-height",
|
||||
format!("{}px", (size as f32 * 1.2 / 72.0 * 48.0).floor()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if style.math_formatting() {
|
||||
// FIXME: Handle math formatting
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/api/richedit/ns-richedit-gettextex
|
||||
// for unicode chars used
|
||||
// unimplemented!()
|
||||
}
|
||||
|
||||
styles
|
||||
}
|
||||
}
|
||||
|
||||
fn is_tag(tag: &str) -> bool {
|
||||
!matches!(tag, "PageDateTime" | "PageTitle")
|
||||
}
|
||||
|
||||
fn fix_newlines(text: &str) -> String {
|
||||
static REGEX_LEADING_SPACES: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"<br>(\s+)").expect("failed to compile regex"));
|
||||
|
||||
let text = text
|
||||
.replace("\u{000b}", "<br>")
|
||||
.replace("\n", "<br>")
|
||||
.replace("\r", "<br>");
|
||||
|
||||
REGEX_LEADING_SPACES
|
||||
.replace_all(&text, |captures: &Captures| {
|
||||
"<br>".to_string() + &" ".repeat(captures[1].len())
|
||||
})
|
||||
.to_string()
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
use crate::page::Renderer;
|
||||
use crate::parser::contents::{OutlineElement, Table, TableCell};
|
||||
use crate::utils::{px, AttributeSet, StyleSet};
|
||||
use color_eyre::Result;
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub(crate) fn render_table(&mut self, table: &Table) -> Result<String> {
|
||||
let mut content = String::new();
|
||||
let mut styles = StyleSet::new();
|
||||
styles.set("border-collapse", "collapse".to_string());
|
||||
|
||||
if table.borders_visible() {
|
||||
styles.set("border", "1pt solid #A3A3A3".to_string());
|
||||
}
|
||||
|
||||
let mut attributes = AttributeSet::new();
|
||||
attributes.set("style", styles.to_string());
|
||||
attributes.set("cellspacing", "0".to_string());
|
||||
attributes.set("cellpadding", "0".to_string());
|
||||
|
||||
if table.borders_visible() {
|
||||
attributes.set("border", "1".to_string());
|
||||
}
|
||||
|
||||
content.push_str(&format!("<table {}>", attributes.to_string()));
|
||||
|
||||
let locked_cols = calc_locked_cols(table.cols_locked(), table.cols());
|
||||
|
||||
let mut col_widths = table.col_widths().to_vec();
|
||||
col_widths.extend(vec![0.0; table.cols() as usize - col_widths.len()].into_iter());
|
||||
let col_widths = &*col_widths;
|
||||
|
||||
for row in table.contents() {
|
||||
content.push_str("<tr>");
|
||||
|
||||
assert_eq!(row.contents().len(), col_widths.len());
|
||||
|
||||
let cells = row
|
||||
.contents()
|
||||
.iter()
|
||||
.zip(col_widths.iter().copied())
|
||||
.zip(locked_cols.iter().copied())
|
||||
.map(|((cell, width), locked)| {
|
||||
if locked {
|
||||
(cell, Some(width))
|
||||
} else {
|
||||
(cell, None)
|
||||
}
|
||||
});
|
||||
|
||||
for (cell, width) in cells {
|
||||
self.render_table_cell(&mut content, cell, width)?;
|
||||
}
|
||||
|
||||
content.push_str("</tr>");
|
||||
}
|
||||
|
||||
content.push_str("</table>");
|
||||
|
||||
Ok(self.render_with_note_tags(table.note_tags(), content))
|
||||
}
|
||||
|
||||
fn render_table_cell(
|
||||
&mut self,
|
||||
contents: &mut String,
|
||||
cell: &TableCell,
|
||||
width: Option<f32>,
|
||||
) -> Result<()> {
|
||||
let mut styles = StyleSet::new();
|
||||
styles.set("padding", "2pt".to_string());
|
||||
styles.set("vertical-align", "top".to_string());
|
||||
styles.set("min-width", px(1.0));
|
||||
|
||||
if let Some(width) = width {
|
||||
styles.set("width", px(width));
|
||||
}
|
||||
|
||||
if let Some(color) = cell.background_color() {
|
||||
styles.set(
|
||||
"background",
|
||||
format!("rgb({}, {}, {})", color.r(), color.g(), color.b()),
|
||||
)
|
||||
}
|
||||
|
||||
let mut attrs = AttributeSet::new();
|
||||
attrs.set("style", styles.to_string());
|
||||
|
||||
contents.push_str(&format!("<td {}>", attrs.to_string()));
|
||||
|
||||
let cell_level = self.table_cell_level(cell.contents());
|
||||
|
||||
let elements = cell.contents().iter().map(|el| (el, 0, cell_level));
|
||||
contents.push_str(&self.render_list(elements, cell.outline_indent_distance().value())?);
|
||||
|
||||
contents.push_str("</td>");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn table_cell_level(&self, elements: &[OutlineElement]) -> u8 {
|
||||
let needs_nesting = elements
|
||||
.iter()
|
||||
.any(|element| self.is_list(element) || self.has_note_tag(element));
|
||||
|
||||
if needs_nesting {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calc_locked_cols(data: &[u8], count: u32) -> Vec<bool> {
|
||||
if data.is_empty() {
|
||||
return vec![false; count as usize];
|
||||
}
|
||||
|
||||
(0..count)
|
||||
.map(|i| data[i as usize / 8] & (1 << (i % 8)) == 1)
|
||||
.collect()
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
//! OneNote parsing error handling.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::{io, string};
|
||||
use thiserror::Error;
|
||||
|
||||
/// The result of parsing a OneNote file.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A parsing error.
|
||||
///
|
||||
/// If the crate is compiled with the `backtrace` feature enabled, the
|
||||
/// parsing error struct will contain a backtrace of the location where
|
||||
/// the error occured. The backtrace can be accessed using
|
||||
/// [`std::error::Error::backtrace()`].
|
||||
#[derive(Error, Debug)]
|
||||
#[error("{kind}")]
|
||||
pub struct Error {
|
||||
kind: ErrorKind,
|
||||
}
|
||||
|
||||
impl From<ErrorKind> for Error {
|
||||
fn from(kind: ErrorKind) -> Self {
|
||||
Error { kind }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
ErrorKind::from(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf16Error> for Error {
|
||||
fn from(err: std::string::FromUtf16Error) -> Self {
|
||||
ErrorKind::from(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<widestring::error::MissingNulTerminator> for Error {
|
||||
fn from(err: widestring::error::MissingNulTerminator) -> Self {
|
||||
ErrorKind::from(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uuid::Error> for Error {
|
||||
fn from(err: uuid::Error) -> Self {
|
||||
ErrorKind::from(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Details about a parsing error
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ErrorKind {
|
||||
/// Hit the end of the OneNote file before it was expected.
|
||||
#[error("Unexpected end of file")]
|
||||
UnexpectedEof,
|
||||
|
||||
/// The parser was asked to process a table-of-contents file that turned out not to be one.
|
||||
#[error("Not a table of contents file: {file}")]
|
||||
NotATocFile { file: String },
|
||||
|
||||
/// The parser was asked to process a section file that turned out not to be one.
|
||||
#[error("Not a section file: {file}")]
|
||||
NotASectionFile { file: String },
|
||||
|
||||
/// When parsing a section group the table-of-contents file for this group was found to be missing.
|
||||
#[error("Table of contents file is missing in dir {dir}")]
|
||||
TocFileMissing { dir: String },
|
||||
|
||||
/// Malformed data was encountered when parsing the OneNote file.
|
||||
#[error("Malformed data: {0}")]
|
||||
MalformedData(Cow<'static, str>),
|
||||
|
||||
/// Malformed data was encountered when parsing the OneNote data.
|
||||
#[error("Malformed OneNote data: {0}")]
|
||||
MalformedOneNoteData(Cow<'static, str>),
|
||||
|
||||
/// Malformed data was encountered when parsing the OneNote file contents.
|
||||
#[error("Malformed OneNote file data: {0}")]
|
||||
MalformedOneNoteFileData(Cow<'static, str>),
|
||||
|
||||
/// Malformed data was encountered when parsing the OneNote file contents.
|
||||
#[error("Malformed OneNote incorrect type: {0}")]
|
||||
MalformedOneNoteIncorrectType(String),
|
||||
|
||||
/// Malformed data was encountered when parsing the OneStore data.
|
||||
#[error("Malformed OneStore data: {0}")]
|
||||
MalformedOneStoreData(Cow<'static, str>),
|
||||
|
||||
/// Malformed data was encountered when parsing the FSSHTTPB data.
|
||||
#[error("Malformed FSSHTTPB data: {0}")]
|
||||
MalformedFssHttpBData(Cow<'static, str>),
|
||||
|
||||
/// A malformed UUID was encountered
|
||||
#[error("Invalid UUID: {err}")]
|
||||
InvalidUuid {
|
||||
#[from]
|
||||
err: uuid::Error,
|
||||
},
|
||||
|
||||
/// An I/O failure was encountered during parsing.
|
||||
#[error("I/O failure: {err}")]
|
||||
IO {
|
||||
#[from]
|
||||
err: io::Error,
|
||||
},
|
||||
|
||||
/// A malformed UTF-16 string was encountered during parsing.
|
||||
#[error("Malformed UTF-16 string: {err}")]
|
||||
Utf16Error {
|
||||
#[from]
|
||||
err: string::FromUtf16Error,
|
||||
},
|
||||
|
||||
/// A UTF-16 string without a null terminator was encountered during parsing.
|
||||
#[error("UTF-16 string is missing null terminator: {err}")]
|
||||
Utf16MissingNull {
|
||||
#[from]
|
||||
err: widestring::error::MissingNulTerminator,
|
||||
},
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use crate::parser::errors::Result;
|
||||
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
|
||||
use crate::parser::Reader;
|
||||
|
||||
/// A byte array with the length determined by a `CompactU64`.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.3].
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.3]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/6bdda105-af7f-4757-8dbe-0c7f3100647e
|
||||
pub(crate) struct BinaryItem(Vec<u8>);
|
||||
|
||||
impl BinaryItem {
|
||||
pub(crate) fn parse(reader: Reader) -> Result<BinaryItem> {
|
||||
let size = CompactU64::parse(reader)?.value();
|
||||
let data = reader.read(size as usize)?.to_vec();
|
||||
|
||||
Ok(BinaryItem(data))
|
||||
}
|
||||
|
||||
pub(crate) fn value(self) -> Vec<u8> {
|
||||
self.0
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
use crate::parser::errors::Result;
|
||||
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
|
||||
use crate::parser::fsshttpb::data::exguid::ExGuid;
|
||||
use crate::parser::Reader;
|
||||
|
||||
/// A FSSHTTP cell identifier.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.10] and [\[MS-FSSHTTPB\] 2.2.1.11].
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.10]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/75bf8297-ef9c-458a-95a3-ad6265bfa864
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.11]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d3f4d22d-6fb4-4032-8587-f3eb9c256e45
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CellId(pub ExGuid, pub ExGuid);
|
||||
|
||||
impl CellId {
|
||||
pub(crate) fn parse(reader: Reader) -> Result<CellId> {
|
||||
let first = ExGuid::parse(reader)?;
|
||||
let second = ExGuid::parse(reader)?;
|
||||
|
||||
Ok(CellId(first, second))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_array(reader: Reader) -> Result<Vec<CellId>> {
|
||||
let mut values = vec![];
|
||||
|
||||
let count = CompactU64::parse(reader)?.value();
|
||||
for _ in 0..count {
|
||||
values.push(CellId::parse(reader)?);
|
||||
}
|
||||
|
||||
Ok(values)
|
||||
}
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::Reader;
|
||||
|
||||
/// A compact unsigned 64-bit integer.
|
||||
///
|
||||
/// The first byte encodes the total width of the integer. If the first byte is zero, there is no
|
||||
/// further data and the integer value is zero. Otherwise the index of the lowest bit with value 1
|
||||
/// of the first byte indicates the width of the remaining integer data:
|
||||
/// If the lowest bit is set, the integer data is 1 byte wide; if the second bit is set, the
|
||||
/// integer data is 2 bytes wide etc.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.1].
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/8eb74ebe-81d1-4569-a29a-308a6128a52f
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CompactU64(u64);
|
||||
|
||||
impl CompactU64 {
|
||||
pub(crate) fn value(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub(crate) fn parse(reader: Reader) -> Result<CompactU64> {
|
||||
let bytes = reader.bytes();
|
||||
|
||||
let first_byte = bytes.first().copied().ok_or(ErrorKind::UnexpectedEof)?;
|
||||
|
||||
if first_byte == 0 {
|
||||
reader.advance(1)?;
|
||||
|
||||
return Ok(CompactU64(0));
|
||||
}
|
||||
|
||||
if first_byte & 1 != 0 {
|
||||
return Ok(CompactU64((reader.get_u8()? >> 1) as u64));
|
||||
}
|
||||
|
||||
if first_byte & 2 != 0 {
|
||||
return Ok(CompactU64((reader.get_u16()? >> 2) as u64));
|
||||
}
|
||||
|
||||
if first_byte & 4 != 0 {
|
||||
if reader.remaining() < 3 {
|
||||
return Err(ErrorKind::UnexpectedEof.into());
|
||||
}
|
||||
|
||||
let value = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], 0]);
|
||||
|
||||
reader.advance(3)?;
|
||||
|
||||
return Ok(CompactU64((value >> 3) as u64));
|
||||
}
|
||||
|
||||
if first_byte & 8 != 0 {
|
||||
if reader.remaining() < 4 {
|
||||
return Err(ErrorKind::UnexpectedEof.into());
|
||||
}
|
||||
|
||||
let value = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
|
||||
|
||||
reader.advance(4)?;
|
||||
|
||||
return Ok(CompactU64((value >> 4) as u64));
|
||||
}
|
||||
|
||||
if first_byte & 16 != 0 {
|
||||
if reader.remaining() < 5 {
|
||||
return Err(ErrorKind::UnexpectedEof.into());
|
||||
}
|
||||
|
||||
let value =
|
||||
u64::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], 0, 0, 0]);
|
||||
|
||||
reader.advance(5)?;
|
||||
|
||||
return Ok(CompactU64(value >> 5));
|
||||
}
|
||||
|
||||
if first_byte & 32 != 0 {
|
||||
if reader.remaining() < 6 {
|
||||
return Err(ErrorKind::UnexpectedEof.into());
|
||||
}
|
||||
|
||||
let value = u64::from_le_bytes([
|
||||
first_byte, bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], 0, 0,
|
||||
]);
|
||||
|
||||
reader.advance(6)?;
|
||||
|
||||
return Ok(CompactU64(value >> 6));
|
||||
}
|
||||
|
||||
if first_byte & 64 != 0 {
|
||||
if reader.remaining() < 7 {
|
||||
return Err(ErrorKind::UnexpectedEof.into());
|
||||
}
|
||||
|
||||
let value = u64::from_le_bytes([
|
||||
first_byte, bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], 0,
|
||||
]);
|
||||
|
||||
reader.advance(7)?;
|
||||
|
||||
return Ok(CompactU64(value >> 7));
|
||||
}
|
||||
|
||||
if first_byte & 128 != 0 {
|
||||
reader.advance(1)?;
|
||||
|
||||
return Ok(CompactU64(reader.get_u64()?));
|
||||
}
|
||||
|
||||
panic!("unexpected compact u64 type: {:x}", first_byte)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
|
||||
use crate::parser::reader::Reader;
|
||||
|
||||
#[test]
|
||||
fn test_zero() {
|
||||
assert_eq!(
|
||||
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_7_bit() {
|
||||
assert_eq!(
|
||||
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_14_bit() {
|
||||
assert_eq!(
|
||||
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_21_bit() {
|
||||
assert_eq!(
|
||||
CompactU64::parse(&mut Reader::new(&[0xd4u8, 0x8b, 0x10]))
|
||||
.unwrap()
|
||||
.value(),
|
||||
135546
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_28_bit() {
|
||||
assert_eq!(
|
||||
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_35_bit() {
|
||||
assert_eq!(
|
||||
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_42_bit() {
|
||||
assert_eq!(
|
||||
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_49_bit() {
|
||||
assert_eq!(
|
||||
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_64_bit() {
|
||||
assert_eq!(
|
||||
CompactU64::parse(&mut Reader::new(&[0u8])).unwrap().value(),
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
|
||||
use crate::parser::shared::guid::Guid;
|
||||
use crate::parser::Reader;
|
||||
use std::fmt;
|
||||
|
||||
/// A variable-width encoding of an extended GUID (GUID + 32 bit value)
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.7].
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.7]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/bff58e9f-8222-4fbb-b112-5826d5febedd
|
||||
#[derive(Clone, Copy, PartialEq, Hash, Eq)]
|
||||
pub struct ExGuid {
|
||||
pub guid: Guid,
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
impl ExGuid {
|
||||
pub fn fallback() -> ExGuid {
|
||||
return ExGuid {
|
||||
guid: Guid::nil(),
|
||||
value: 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn is_nil(&self) -> bool {
|
||||
self.guid.is_nil() && self.value == 0
|
||||
}
|
||||
|
||||
pub(crate) fn as_option(&self) -> Option<ExGuid> {
|
||||
if self.is_nil() {
|
||||
None
|
||||
} else {
|
||||
Some(*self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_guid(guid: Guid, value: u32) -> ExGuid {
|
||||
ExGuid { guid, value }
|
||||
}
|
||||
|
||||
pub(crate) fn parse(reader: Reader) -> Result<ExGuid> {
|
||||
let data = reader.get_u8()?;
|
||||
|
||||
// A null ExGuid ([FSSHTTPB] 2.2.1.7.1)
|
||||
if data == 0 {
|
||||
return Ok(ExGuid {
|
||||
guid: Guid::nil(),
|
||||
value: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// A ExGuid with a 5 bit value ([FSSHTTPB] 2.2.1.7.2)
|
||||
if data & 0b111 == 4 {
|
||||
return Ok(ExGuid {
|
||||
guid: Guid::parse(reader)?,
|
||||
value: (data >> 3) as u32,
|
||||
});
|
||||
}
|
||||
|
||||
// A ExGuid with a 10 bit value ([FSSHTTPB] 2.2.1.7.3)
|
||||
if data & 0b111111 == 32 {
|
||||
let value = (reader.get_u8()? as u16) << 2 | (data >> 6) as u16;
|
||||
|
||||
return Ok(ExGuid {
|
||||
guid: Guid::parse(reader)?,
|
||||
value: value as u32,
|
||||
});
|
||||
}
|
||||
|
||||
// A ExGuid with a 17 bit value ([FSSHTTPB] 2.2.1.7.4)
|
||||
if data & 0b1111111 == 64 {
|
||||
let value = (reader.get_u16()? as u32) << 1 | (data >> 7) as u32;
|
||||
|
||||
return Ok(ExGuid {
|
||||
guid: Guid::parse(reader)?,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
// A ExGuid with a 32 bit value ([FSSHTTPB] 2.2.1.7.5)
|
||||
if data == 128 {
|
||||
let value = reader.get_u32()?;
|
||||
|
||||
return Ok(ExGuid {
|
||||
guid: Guid::parse(reader)?,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
Err(
|
||||
ErrorKind::MalformedData(format!("unexpected ExGuid first byte: {:b}", data).into())
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse an array of `ExGuid` values.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.8]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.8]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/10d6fb35-d630-4ae3-b530-b9e877fc27d3
|
||||
pub(crate) fn parse_array(reader: Reader) -> Result<Vec<ExGuid>> {
|
||||
let mut values = vec![];
|
||||
|
||||
let count = CompactU64::parse(reader)?.value();
|
||||
for _ in 0..count {
|
||||
values.push(ExGuid::parse(reader)?);
|
||||
}
|
||||
|
||||
Ok(values)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ExGuid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ExGuid {{{}, {}}}", self.guid, self.value)
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
pub(crate) mod binary_item;
|
||||
pub(crate) mod cell_id;
|
||||
pub(crate) mod compact_u64;
|
||||
pub(crate) mod exguid;
|
||||
pub(crate) mod object_types;
|
||||
pub(crate) mod serial_number;
|
||||
pub(crate) mod stream_object;
|
@ -1,51 +0,0 @@
|
||||
use enum_primitive_derive::Primitive;
|
||||
use num_traits::ToPrimitive;
|
||||
use std::fmt;
|
||||
|
||||
/// Stream object types.
|
||||
///
|
||||
/// While the FSSHTTPB protocol specified more object types than listed here, we only need a limited
|
||||
/// number of them to parse OneNote files stored in FSSHTTPB format.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.5.1] and [\[MS-FSSHTTPB\] 2.2.1.5.2].
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.5.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/a1017f48-a888-49ff-b71d-cc3c707f753a
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.5.2]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/ac629d63-60a1-49b2-9db2-fa3c19971cc9
|
||||
#[derive(Debug, Primitive, PartialEq)]
|
||||
pub enum ObjectType {
|
||||
CellManifest = 0x0B,
|
||||
DataElement = 0x01,
|
||||
DataElementFragment = 0x06A,
|
||||
DataElementPackage = 0x15,
|
||||
ObjectDataBlob = 0x02,
|
||||
ObjectGroupBlobReference = 0x1C,
|
||||
ObjectGroupData = 0x1E,
|
||||
ObjectGroupDataBlob = 0x05,
|
||||
ObjectGroupDataExcluded = 0x03,
|
||||
ObjectGroupDataObject = 0x16,
|
||||
ObjectGroupDeclaration = 0x1D,
|
||||
ObjectGroupMetadata = 0x078,
|
||||
ObjectGroupMetadataBlock = 0x79,
|
||||
ObjectGroupObject = 0x18,
|
||||
/// An indicator that the object contains a OneNote packing object.
|
||||
///
|
||||
/// See [\[MS-ONESTORE\] 2.8.1] (look for _Packaging Start_)
|
||||
///
|
||||
/// [\[MS-ONESTORE\] 2.8.1]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-onestore/a2f046ea-109a-49c4-912d-dc2888cf0565
|
||||
OneNotePackaging = 0x7a,
|
||||
RevisionManifest = 0x1A,
|
||||
RevisionManifestGroupReference = 0x19,
|
||||
RevisionManifestRoot = 0x0A,
|
||||
StorageIndexCellMapping = 0x0E,
|
||||
StorageIndexManifestMapping = 0x11,
|
||||
StorageIndexRevisionMapping = 0x0D,
|
||||
StorageManifest = 0x0C,
|
||||
StorageManifestRoot = 0x07,
|
||||
}
|
||||
|
||||
impl fmt::LowerHex for ObjectType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let value = self.to_u64().unwrap();
|
||||
fmt::LowerHex::fmt(&value, f)
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
use crate::parser::errors::Result;
|
||||
use crate::parser::shared::guid::Guid;
|
||||
use crate::parser::Reader;
|
||||
|
||||
/// A variable-width serial number.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.9].
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.9]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/9db15fa4-0dc2-4b17-b091-d33886d8a0f6
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct SerialNumber {
|
||||
pub guid: Guid,
|
||||
pub serial: u64,
|
||||
}
|
||||
|
||||
impl SerialNumber {
|
||||
pub(crate) fn parse(reader: Reader) -> Result<SerialNumber> {
|
||||
let serial_type = reader.get_u8()?;
|
||||
|
||||
// A null-value ([FSSHTTPB] 2.2.1.9.1)
|
||||
if serial_type == 0 {
|
||||
return Ok(SerialNumber {
|
||||
guid: Guid::nil(),
|
||||
serial: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// A serial number with a 64 bit value ([FSSHTTPB] 2.2.1.9.2)
|
||||
let guid = Guid::parse(reader)?;
|
||||
let serial = reader.get_u64()?;
|
||||
|
||||
Ok(SerialNumber { guid, serial })
|
||||
}
|
||||
}
|
@ -1,234 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
|
||||
use crate::parser::fsshttpb::data::object_types::ObjectType;
|
||||
use crate::parser::Reader;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
|
||||
/// A FSSHTTPB stream object header.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.5].
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.5]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/5faee10f-8e55-43f8-935a-d6e4294856fc
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ObjectHeader {
|
||||
pub compound: bool,
|
||||
pub object_type: ObjectType,
|
||||
pub length: u64,
|
||||
}
|
||||
|
||||
impl ObjectHeader {
|
||||
pub(crate) fn try_parse(reader: Reader, object_type: ObjectType) -> Result<()> {
|
||||
Self::try_parse_start(reader, object_type, Self::parse)
|
||||
}
|
||||
|
||||
/// Parse a 16-bit or 32-bit stream object header.
|
||||
pub(crate) fn parse(reader: Reader) -> Result<ObjectHeader> {
|
||||
let header_type = reader.bytes().first().ok_or(ErrorKind::UnexpectedEof)?;
|
||||
|
||||
match header_type & 0b11 {
|
||||
0x0 => Self::parse_16(reader),
|
||||
0x2 => Self::parse_32(reader),
|
||||
_ => Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("unexpected object header type: {:x}", header_type).into(),
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn try_parse_16(reader: Reader, object_type: ObjectType) -> Result<()> {
|
||||
Self::try_parse_start(reader, object_type, Self::parse_16)
|
||||
}
|
||||
|
||||
/// Parse a 16 bit stream object header.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.5.1]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.5.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/a1017f48-a888-49ff-b71d-cc3c707f753a
|
||||
pub(crate) fn parse_16(reader: Reader) -> Result<ObjectHeader> {
|
||||
let data = reader.get_u16()?;
|
||||
|
||||
let header_type = data & 0b11;
|
||||
if header_type != 0x0 {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!(
|
||||
"unexpected object header type for 16 bit header: 0x{:x}",
|
||||
header_type
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let compound = data & 0x4 == 0x4;
|
||||
let object_type_value = (data >> 3) & 0x3f;
|
||||
let object_type = if let Some(object_type) = ObjectType::from_u16(object_type_value) {
|
||||
object_type
|
||||
} else {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("invalid object type: 0x{:x}", object_type_value).into(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
let length = (data >> 9) as u64;
|
||||
|
||||
Ok(ObjectHeader {
|
||||
compound,
|
||||
object_type,
|
||||
length,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn try_parse_32(reader: Reader, object_type: ObjectType) -> Result<()> {
|
||||
Self::try_parse_start(reader, object_type, Self::parse_32)
|
||||
}
|
||||
|
||||
/// Parse a 32 bit stream object header.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.5.2]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.5.2]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/ac629d63-60a1-49b2-9db2-fa3c19971cc9
|
||||
fn parse_32(reader: Reader) -> Result<ObjectHeader> {
|
||||
let data = reader.get_u32()?;
|
||||
|
||||
let header_type = data & 0b11;
|
||||
if header_type != 0x2 {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!(
|
||||
"unexpected object header type for 32 bit header: 0x{:x}",
|
||||
header_type
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let compound = data & 0x4 == 0x4;
|
||||
let object_type_value = (data >> 3) & 0x3fff;
|
||||
let object_type = if let Some(object_type) = ObjectType::from_u32(object_type_value) {
|
||||
object_type
|
||||
} else {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("invalid object type: 0x{:x}", object_type_value).into(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
let mut length = (data >> 17) as u64;
|
||||
|
||||
if length == 0x7fff {
|
||||
length = CompactU64::parse(reader)?.value();
|
||||
}
|
||||
|
||||
Ok(ObjectHeader {
|
||||
compound,
|
||||
object_type,
|
||||
length,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn try_parse_end_16(reader: Reader, object_type: ObjectType) -> Result<()> {
|
||||
Self::try_parse_end(reader, object_type, Self::parse_end_16)
|
||||
}
|
||||
|
||||
/// Parse a 16-bit stream object header end.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.5.4]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.5.4]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d8cedbb8-073b-4711-8867-f88b887ab0a9
|
||||
fn parse_end_16(reader: Reader) -> Result<ObjectType> {
|
||||
let data = reader.get_u16()?;
|
||||
let header_type = data & 0b11;
|
||||
if header_type != 0x3 {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!(
|
||||
"unexpected object header type for 16 bit end header: {:x}",
|
||||
header_type
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let object_type_value = data >> 2;
|
||||
|
||||
if let Some(object_type) = ObjectType::from_u16(object_type_value) {
|
||||
Ok(object_type)
|
||||
} else {
|
||||
Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("invalid object type: 0x{:x}", object_type_value).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn try_parse_end_8(reader: Reader, object_type: ObjectType) -> Result<()> {
|
||||
Self::try_parse_end(reader, object_type, Self::parse_end_8)
|
||||
}
|
||||
|
||||
/// Parse a 8-bit stream object header end.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.5.3]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.5.3]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/544ce81a-44e3-48ff-b094-0e51c7207aa1
|
||||
fn parse_end_8(reader: Reader) -> Result<ObjectType> {
|
||||
let data = reader.get_u8()?;
|
||||
let header_type = data & 0b11;
|
||||
if header_type != 0x1 {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!(
|
||||
"unexpected object header type for 8 bit end header: {:x}",
|
||||
header_type
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let object_type_value = data >> 2;
|
||||
|
||||
if let Some(object_type) = ObjectType::from_u8(object_type_value) {
|
||||
Ok(object_type)
|
||||
} else {
|
||||
Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("invalid object type: 0x{:x}", object_type_value).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_end_8(reader: Reader, object_type: ObjectType) -> Result<bool> {
|
||||
let data = reader.bytes().first().ok_or(ErrorKind::UnexpectedEof)?;
|
||||
|
||||
Ok(data & 0b11 == 0x1 && data >> 2 == object_type.to_u8().unwrap())
|
||||
}
|
||||
|
||||
fn try_parse_start(
|
||||
reader: Reader,
|
||||
object_type: ObjectType,
|
||||
parse: fn(Reader) -> Result<ObjectHeader>,
|
||||
) -> Result<()> {
|
||||
match parse(reader) {
|
||||
Ok(header) if header.object_type == object_type => Ok(()),
|
||||
Ok(header) => Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("unexpected object type: {:x}", header.object_type).into(),
|
||||
)
|
||||
.into()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_parse_end(
|
||||
reader: Reader,
|
||||
object_type: ObjectType,
|
||||
parse: fn(Reader) -> Result<ObjectType>,
|
||||
) -> Result<()> {
|
||||
match parse(reader) {
|
||||
Ok(header) if header == object_type => Ok(()),
|
||||
Ok(header) => Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("unexpected object type: {:x}", header).into(),
|
||||
)
|
||||
.into()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use crate::parser::errors::Result;
|
||||
use crate::parser::fsshttpb::data::exguid::ExGuid;
|
||||
use crate::parser::fsshttpb::data::object_types::ObjectType;
|
||||
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
|
||||
use crate::parser::fsshttpb::data_element::DataElement;
|
||||
use crate::parser::Reader;
|
||||
|
||||
impl DataElement {
|
||||
/// Parse a cell manifest.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.4]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.4]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/541f7f92-ee5d-407e-9ece-fb1b35832a10
|
||||
pub(crate) fn parse_cell_manifest(reader: Reader) -> Result<ExGuid> {
|
||||
ObjectHeader::try_parse_16(reader, ObjectType::CellManifest)?;
|
||||
|
||||
let id = ExGuid::parse(reader)?;
|
||||
|
||||
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
use crate::parser::errors::Result;
|
||||
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
|
||||
use crate::parser::fsshttpb::data::exguid::ExGuid;
|
||||
use crate::parser::fsshttpb::data::object_types::ObjectType;
|
||||
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
|
||||
use crate::parser::fsshttpb::data_element::DataElement;
|
||||
use crate::parser::Reader;
|
||||
|
||||
/// A data element fragment.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.7].
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.7]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/9a860e3b-cf61-484b-8ee3-d875afaf7a05
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct DataElementFragment {
|
||||
pub(crate) id: ExGuid,
|
||||
pub(crate) size: u64,
|
||||
pub(crate) chunk_reference: DataElementFragmentChunkReference,
|
||||
pub(crate) data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct DataElementFragmentChunkReference {
|
||||
pub(crate) offset: u64,
|
||||
pub(crate) length: u64,
|
||||
}
|
||||
|
||||
impl DataElement {
|
||||
/// Parse a data element fragment.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.7]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.7]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/9a860e3b-cf61-484b-8ee3-d875afaf7a05
|
||||
pub(crate) fn parse_data_element_fragment(reader: Reader) -> Result<DataElementFragment> {
|
||||
ObjectHeader::try_parse(reader, ObjectType::DataElementFragment)?;
|
||||
|
||||
let id = ExGuid::parse(reader)?;
|
||||
let size = CompactU64::parse(reader)?.value();
|
||||
let offset = CompactU64::parse(reader)?.value();
|
||||
let length = CompactU64::parse(reader)?.value();
|
||||
|
||||
let data = reader.read(size as usize)?.to_vec();
|
||||
|
||||
let chunk_reference = DataElementFragmentChunkReference { offset, length };
|
||||
let fragment = DataElementFragment {
|
||||
id,
|
||||
size,
|
||||
chunk_reference,
|
||||
data,
|
||||
};
|
||||
|
||||
Ok(fragment)
|
||||
}
|
||||
}
|
@ -1,196 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
|
||||
use crate::parser::fsshttpb::data::exguid::ExGuid;
|
||||
use crate::parser::fsshttpb::data::object_types::ObjectType;
|
||||
use crate::parser::fsshttpb::data::serial_number::SerialNumber;
|
||||
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
|
||||
use crate::parser::fsshttpb::data_element::data_element_fragment::DataElementFragment;
|
||||
use crate::parser::fsshttpb::data_element::object_data_blob::ObjectDataBlob;
|
||||
use crate::parser::fsshttpb::data_element::object_group::ObjectGroup;
|
||||
use crate::parser::fsshttpb::data_element::revision_manifest::RevisionManifest;
|
||||
use crate::parser::fsshttpb::data_element::storage_index::StorageIndex;
|
||||
use crate::parser::fsshttpb::data_element::storage_manifest::StorageManifest;
|
||||
use crate::parser::Reader;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub(crate) mod cell_manifest;
|
||||
pub(crate) mod data_element_fragment;
|
||||
pub(crate) mod object_data_blob;
|
||||
pub(crate) mod object_group;
|
||||
pub(crate) mod revision_manifest;
|
||||
pub(crate) mod storage_index;
|
||||
pub(crate) mod storage_manifest;
|
||||
|
||||
/// A FSSHTTPB data element package.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12].
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/99a25464-99b5-4262-a964-baabed2170eb
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DataElementPackage {
|
||||
pub(crate) storage_indexes: HashMap<ExGuid, StorageIndex>,
|
||||
pub(crate) storage_manifests: HashMap<ExGuid, StorageManifest>,
|
||||
pub(crate) cell_manifests: HashMap<ExGuid, ExGuid>,
|
||||
pub(crate) revision_manifests: HashMap<ExGuid, RevisionManifest>,
|
||||
pub(crate) object_groups: HashMap<ExGuid, ObjectGroup>,
|
||||
pub(crate) data_element_fragments: HashMap<ExGuid, DataElementFragment>,
|
||||
pub(crate) object_data_blobs: HashMap<ExGuid, ObjectDataBlob>,
|
||||
}
|
||||
|
||||
impl DataElementPackage {
|
||||
pub(crate) fn parse(reader: Reader) -> Result<DataElementPackage> {
|
||||
ObjectHeader::try_parse_16(reader, ObjectType::DataElementPackage)?;
|
||||
|
||||
if reader.get_u8()? != 0 {
|
||||
return Err(ErrorKind::MalformedFssHttpBData("invalid padding byte".into()).into());
|
||||
}
|
||||
|
||||
let mut package = DataElementPackage {
|
||||
storage_indexes: Default::default(),
|
||||
storage_manifests: Default::default(),
|
||||
cell_manifests: Default::default(),
|
||||
revision_manifests: Default::default(),
|
||||
object_groups: Default::default(),
|
||||
data_element_fragments: Default::default(),
|
||||
object_data_blobs: Default::default(),
|
||||
};
|
||||
|
||||
loop {
|
||||
if ObjectHeader::has_end_8(reader, ObjectType::DataElementPackage)? {
|
||||
break;
|
||||
}
|
||||
|
||||
DataElement::parse(reader, &mut package)?
|
||||
}
|
||||
|
||||
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElementPackage)?;
|
||||
|
||||
Ok(package)
|
||||
}
|
||||
|
||||
/// Look up the object groups referenced by a cell.
|
||||
pub(crate) fn find_objects(
|
||||
&self,
|
||||
cell: ExGuid,
|
||||
storage_index: &StorageIndex,
|
||||
) -> Result<Vec<&ObjectGroup>> {
|
||||
let revision_id = self
|
||||
.find_cell_revision_id(cell)
|
||||
.ok_or_else(|| ErrorKind::MalformedFssHttpBData("cell revision id not found".into()))?;
|
||||
let revision_mapping_id = storage_index
|
||||
.find_revision_mapping_id(revision_id)
|
||||
.ok_or_else(|| {
|
||||
ErrorKind::MalformedFssHttpBData("revision mapping id not found".into())
|
||||
})?;
|
||||
let revision_manifest = self
|
||||
.find_revision_manifest(revision_mapping_id)
|
||||
.ok_or_else(|| {
|
||||
ErrorKind::MalformedFssHttpBData("revision manifest not found".into())
|
||||
})?;
|
||||
|
||||
revision_manifest
|
||||
.group_references
|
||||
.iter()
|
||||
.map(|reference| {
|
||||
self.find_object_group(*reference).ok_or_else(|| {
|
||||
ErrorKind::MalformedFssHttpBData("object group not found".into()).into()
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()
|
||||
}
|
||||
|
||||
/// Look up a blob by its ID.
|
||||
pub(crate) fn find_blob(&self, id: ExGuid) -> Option<&[u8]> {
|
||||
self.object_data_blobs.get(&id).map(|blob| blob.value())
|
||||
}
|
||||
|
||||
/// Find the first storage index.
|
||||
pub(crate) fn find_storage_index(&self) -> Option<&StorageIndex> {
|
||||
self.storage_indexes.values().next()
|
||||
}
|
||||
|
||||
/// Find the first storage manifest.
|
||||
pub(crate) fn find_storage_manifest(&self) -> Option<&StorageManifest> {
|
||||
self.storage_manifests.values().next()
|
||||
}
|
||||
|
||||
/// Look up a cell revision ID by the cell's manifest ID.
|
||||
pub(crate) fn find_cell_revision_id(&self, id: ExGuid) -> Option<ExGuid> {
|
||||
self.cell_manifests.get(&id).copied()
|
||||
}
|
||||
|
||||
/// Look up a revision manifest by its ID.
|
||||
pub(crate) fn find_revision_manifest(&self, id: ExGuid) -> Option<&RevisionManifest> {
|
||||
self.revision_manifests.get(&id)
|
||||
}
|
||||
|
||||
/// Look up an object group by its ID.
|
||||
pub(crate) fn find_object_group(&self, id: ExGuid) -> Option<&ObjectGroup> {
|
||||
self.object_groups.get(&id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A parser for a single data element.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.1]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/f0901ac0-4f26-413f-805b-a6830781f64c
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DataElement;
|
||||
|
||||
impl DataElement {
|
||||
pub(crate) fn parse(reader: Reader, package: &mut DataElementPackage) -> Result<()> {
|
||||
ObjectHeader::try_parse_16(reader, ObjectType::DataElement)?;
|
||||
|
||||
let id = ExGuid::parse(reader)?;
|
||||
let _serial = SerialNumber::parse(reader)?;
|
||||
let element_type = CompactU64::parse(reader)?;
|
||||
|
||||
match element_type.value() {
|
||||
0x01 => {
|
||||
package
|
||||
.storage_indexes
|
||||
.insert(id, Self::parse_storage_index(reader)?);
|
||||
}
|
||||
0x02 => {
|
||||
package
|
||||
.storage_manifests
|
||||
.insert(id, Self::parse_storage_manifest(reader)?);
|
||||
}
|
||||
0x03 => {
|
||||
package
|
||||
.cell_manifests
|
||||
.insert(id, Self::parse_cell_manifest(reader)?);
|
||||
}
|
||||
0x04 => {
|
||||
package
|
||||
.revision_manifests
|
||||
.insert(id, Self::parse_revision_manifest(reader)?);
|
||||
}
|
||||
0x05 => {
|
||||
package
|
||||
.object_groups
|
||||
.insert(id, Self::parse_object_group(reader)?);
|
||||
}
|
||||
0x06 => {
|
||||
package
|
||||
.data_element_fragments
|
||||
.insert(id, Self::parse_data_element_fragment(reader)?);
|
||||
}
|
||||
0x0A => {
|
||||
package
|
||||
.object_data_blobs
|
||||
.insert(id, Self::parse_object_data_blob(reader)?);
|
||||
}
|
||||
x => {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("invalid element type: 0x{:X}", x).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
use crate::parser::errors::Result;
|
||||
use crate::parser::fsshttpb::data::binary_item::BinaryItem;
|
||||
use crate::parser::fsshttpb::data::object_types::ObjectType;
|
||||
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
|
||||
use crate::parser::fsshttpb::data_element::DataElement;
|
||||
use crate::parser::Reader;
|
||||
use std::fmt;
|
||||
|
||||
/// An object data blob.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.8]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.8]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d36dd2b4-bad1-441b-93c7-adbe3069152c
|
||||
pub(crate) struct ObjectDataBlob(Vec<u8>);
|
||||
|
||||
impl ObjectDataBlob {
|
||||
pub(crate) fn value(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ObjectDataBlob {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ObjectDataBlob({} bytes)", self.0.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl DataElement {
|
||||
pub(crate) fn parse_object_data_blob(reader: Reader) -> Result<ObjectDataBlob> {
|
||||
ObjectHeader::try_parse(reader, ObjectType::ObjectDataBlob)?;
|
||||
|
||||
let data = BinaryItem::parse(reader)?;
|
||||
|
||||
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
|
||||
|
||||
Ok(ObjectDataBlob(data.value()))
|
||||
}
|
||||
}
|
@ -1,336 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::fsshttpb::data::binary_item::BinaryItem;
|
||||
use crate::parser::fsshttpb::data::cell_id::CellId;
|
||||
use crate::parser::fsshttpb::data::compact_u64::CompactU64;
|
||||
use crate::parser::fsshttpb::data::exguid::ExGuid;
|
||||
use crate::parser::fsshttpb::data::object_types::ObjectType;
|
||||
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
|
||||
use crate::parser::fsshttpb::data_element::DataElement;
|
||||
use crate::parser::Reader;
|
||||
use std::fmt;
|
||||
|
||||
/// An object group.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.6]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/21404be6-0334-490e-80b5-82fccb9c04af
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct ObjectGroup {
|
||||
pub(crate) declarations: Vec<ObjectGroupDeclaration>,
|
||||
pub(crate) metadata: Vec<ObjectGroupMetadata>,
|
||||
pub(crate) objects: Vec<ObjectGroupData>,
|
||||
}
|
||||
|
||||
/// An object group declaration.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6.1]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/ef660e4b-a099-4e76-81f7-ed5c04a70caa
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum ObjectGroupDeclaration {
|
||||
Object {
|
||||
object_id: ExGuid,
|
||||
partition_id: u64,
|
||||
data_size: u64,
|
||||
object_reference_count: u64,
|
||||
cell_reference_count: u64,
|
||||
},
|
||||
Blob {
|
||||
object_id: ExGuid,
|
||||
blob_id: ExGuid,
|
||||
partition_id: u64,
|
||||
object_reference_count: u64,
|
||||
cell_reference_count: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl ObjectGroupDeclaration {
|
||||
pub(crate) fn partition_id(&self) -> u64 {
|
||||
match self {
|
||||
ObjectGroupDeclaration::Object { partition_id, .. } => *partition_id,
|
||||
ObjectGroupDeclaration::Blob { partition_id, .. } => *partition_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn object_id(&self) -> ExGuid {
|
||||
match self {
|
||||
ObjectGroupDeclaration::Object { object_id, .. } => *object_id,
|
||||
ObjectGroupDeclaration::Blob { object_id, .. } => *object_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An object group's metadata.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6.3] and [\[MS-FSSHTTPB\] 2.2.1.12.6.3.1]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.3]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d35a8e21-e139-455c-a20b-3f47a5d9fb89
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.3.1]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/507c6b42-2772-4319-b530-8fbbf4d34afd
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct ObjectGroupMetadata {
|
||||
pub(crate) change_frequency: ObjectChangeFrequency,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ObjectChangeFrequency {
|
||||
Unknown = 0,
|
||||
Frequent = 1,
|
||||
Infrequent = 2,
|
||||
Independent = 3,
|
||||
Custom = 4,
|
||||
}
|
||||
|
||||
impl ObjectChangeFrequency {
|
||||
fn parse(value: u64) -> ObjectChangeFrequency {
|
||||
match value {
|
||||
x if x == ObjectChangeFrequency::Unknown as u64 => ObjectChangeFrequency::Unknown,
|
||||
x if x == ObjectChangeFrequency::Frequent as u64 => ObjectChangeFrequency::Frequent,
|
||||
x if x == ObjectChangeFrequency::Infrequent as u64 => ObjectChangeFrequency::Infrequent,
|
||||
x if x == ObjectChangeFrequency::Independent as u64 => {
|
||||
ObjectChangeFrequency::Independent
|
||||
}
|
||||
x if x == ObjectChangeFrequency::Custom as u64 => ObjectChangeFrequency::Custom,
|
||||
x => panic!("unexpected change frequency: {}", x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An object group's data.
|
||||
pub(crate) enum ObjectGroupData {
|
||||
/// An object.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6.4]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.4]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d542b89c-9e81-4af6-885a-47b2f9c1ce53
|
||||
Object {
|
||||
group: Vec<ExGuid>,
|
||||
cells: Vec<CellId>,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
/// An excluded object.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6.4]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.4]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/d542b89c-9e81-4af6-885a-47b2f9c1ce53
|
||||
ObjectExcluded {
|
||||
group: Vec<ExGuid>,
|
||||
cells: Vec<CellId>,
|
||||
size: u64,
|
||||
},
|
||||
/// A blob reference.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.6.5]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.6.5]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/9f73af5e-bd70-4703-8ec6-1866338f1b91
|
||||
BlobReference {
|
||||
objects: Vec<ExGuid>,
|
||||
cells: Vec<CellId>,
|
||||
blob: ExGuid,
|
||||
},
|
||||
}
|
||||
|
||||
struct DebugSize(usize);
|
||||
|
||||
impl fmt::Debug for ObjectGroupData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ObjectGroupData::Object { group, cells, data } => f
|
||||
.debug_struct("Object")
|
||||
.field("group", group)
|
||||
.field("cells", cells)
|
||||
.field("data", &DebugSize(data.len()))
|
||||
.finish(),
|
||||
ObjectGroupData::ObjectExcluded { group, cells, size } => f
|
||||
.debug_struct("ObjectExcluded")
|
||||
.field("group", group)
|
||||
.field("cells", cells)
|
||||
.field("size", size)
|
||||
.finish(),
|
||||
ObjectGroupData::BlobReference {
|
||||
objects,
|
||||
cells,
|
||||
blob,
|
||||
} => f
|
||||
.debug_struct("ObjectExcluded")
|
||||
.field("objects", objects)
|
||||
.field("cells", cells)
|
||||
.field("blob", blob)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DebugSize {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} bytes", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl DataElement {
|
||||
pub(crate) fn parse_object_group(reader: Reader) -> Result<ObjectGroup> {
|
||||
let declarations = DataElement::parse_object_group_declarations(reader)?;
|
||||
|
||||
let mut metadata = vec![];
|
||||
|
||||
let object_header = ObjectHeader::parse(reader)?;
|
||||
match object_header.object_type {
|
||||
ObjectType::ObjectGroupMetadataBlock => {
|
||||
metadata = DataElement::parse_object_group_metadata(reader)?;
|
||||
|
||||
// Parse object header for the group data section
|
||||
let object_header = ObjectHeader::parse(reader)?;
|
||||
if object_header.object_type != ObjectType::ObjectGroupData {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("unexpected object type: {:x}", object_header.object_type).into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
ObjectType::ObjectGroupData => {} // Skip, will be parsed below
|
||||
_ => {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("unexpected object type: {:x}", object_header.object_type).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
let objects = DataElement::parse_object_group_data(reader)?;
|
||||
|
||||
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
|
||||
|
||||
Ok(ObjectGroup {
|
||||
declarations,
|
||||
metadata,
|
||||
objects,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_object_group_declarations(reader: Reader) -> Result<Vec<ObjectGroupDeclaration>> {
|
||||
ObjectHeader::try_parse(reader, ObjectType::ObjectGroupDeclaration)?;
|
||||
|
||||
let mut declarations = vec![];
|
||||
|
||||
loop {
|
||||
if ObjectHeader::has_end_8(reader, ObjectType::ObjectGroupDeclaration)? {
|
||||
break;
|
||||
}
|
||||
|
||||
let object_header = ObjectHeader::parse(reader)?;
|
||||
match object_header.object_type {
|
||||
ObjectType::ObjectGroupObject => {
|
||||
let object_id = ExGuid::parse(reader)?;
|
||||
let partition_id = CompactU64::parse(reader)?.value();
|
||||
let data_size = CompactU64::parse(reader)?.value();
|
||||
let object_reference_count = CompactU64::parse(reader)?.value();
|
||||
let cell_reference_count = CompactU64::parse(reader)?.value();
|
||||
|
||||
declarations.push(ObjectGroupDeclaration::Object {
|
||||
object_id,
|
||||
partition_id,
|
||||
data_size,
|
||||
object_reference_count,
|
||||
cell_reference_count,
|
||||
})
|
||||
}
|
||||
ObjectType::ObjectGroupDataBlob => {
|
||||
let object_id = ExGuid::parse(reader)?;
|
||||
let blob_id = ExGuid::parse(reader)?;
|
||||
let partition_id = CompactU64::parse(reader)?.value();
|
||||
let object_reference_count = CompactU64::parse(reader)?.value();
|
||||
let cell_reference_count = CompactU64::parse(reader)?.value();
|
||||
|
||||
declarations.push(ObjectGroupDeclaration::Blob {
|
||||
object_id,
|
||||
blob_id,
|
||||
partition_id,
|
||||
object_reference_count,
|
||||
cell_reference_count,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("unexpected object type: {:x}", object_header.object_type).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ObjectHeader::try_parse_end_8(reader, ObjectType::ObjectGroupDeclaration)?;
|
||||
|
||||
Ok(declarations)
|
||||
}
|
||||
|
||||
fn parse_object_group_metadata(reader: Reader) -> Result<Vec<ObjectGroupMetadata>> {
|
||||
let mut declarations = vec![];
|
||||
|
||||
loop {
|
||||
if ObjectHeader::has_end_8(reader, ObjectType::ObjectGroupMetadataBlock)? {
|
||||
break;
|
||||
}
|
||||
|
||||
ObjectHeader::try_parse_32(reader, ObjectType::ObjectGroupMetadata)?;
|
||||
|
||||
let frequency = CompactU64::parse(reader)?;
|
||||
declarations.push(ObjectGroupMetadata {
|
||||
change_frequency: ObjectChangeFrequency::parse(frequency.value()),
|
||||
})
|
||||
}
|
||||
|
||||
ObjectHeader::try_parse_end_8(reader, ObjectType::ObjectGroupMetadataBlock)?;
|
||||
|
||||
Ok(declarations)
|
||||
}
|
||||
|
||||
fn parse_object_group_data(reader: Reader) -> Result<Vec<ObjectGroupData>> {
|
||||
let mut objects = vec![];
|
||||
|
||||
loop {
|
||||
if ObjectHeader::has_end_8(reader, ObjectType::ObjectGroupData)? {
|
||||
break;
|
||||
}
|
||||
|
||||
let object_header = ObjectHeader::parse(reader)?;
|
||||
match object_header.object_type {
|
||||
ObjectType::ObjectGroupDataExcluded => {
|
||||
let group = ExGuid::parse_array(reader)?;
|
||||
let cells = CellId::parse_array(reader)?;
|
||||
let size = CompactU64::parse(reader)?.value();
|
||||
|
||||
objects.push(ObjectGroupData::ObjectExcluded { group, cells, size })
|
||||
}
|
||||
ObjectType::ObjectGroupDataObject => {
|
||||
let group = ExGuid::parse_array(reader)?;
|
||||
let cells = CellId::parse_array(reader)?;
|
||||
let data = BinaryItem::parse(reader)?.value();
|
||||
|
||||
objects.push(ObjectGroupData::Object { group, cells, data })
|
||||
}
|
||||
ObjectType::ObjectGroupBlobReference => {
|
||||
let references = ExGuid::parse_array(reader)?;
|
||||
let cells = CellId::parse_array(reader)?;
|
||||
let blob = ExGuid::parse(reader)?;
|
||||
|
||||
objects.push(ObjectGroupData::BlobReference {
|
||||
objects: references,
|
||||
cells,
|
||||
blob,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("unexpected object type: {:x}", object_header.object_type).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ObjectHeader::try_parse_end_8(reader, ObjectType::ObjectGroupData)?;
|
||||
|
||||
Ok(objects)
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::fsshttpb::data::exguid::ExGuid;
|
||||
use crate::parser::fsshttpb::data::object_types::ObjectType;
|
||||
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
|
||||
use crate::parser::fsshttpb::data_element::DataElement;
|
||||
use crate::parser::Reader;
|
||||
|
||||
/// A revision manifest.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.5]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.5]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/eb3351db-8626-4804-a35b-f3eeda13c74d
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RevisionManifest {
|
||||
pub(crate) rev_id: ExGuid,
|
||||
pub(crate) base_rev_id: ExGuid,
|
||||
pub(crate) root_declare: Vec<RevisionManifestRootDeclare>,
|
||||
pub(crate) group_references: Vec<ExGuid>,
|
||||
}
|
||||
|
||||
/// A revision manifest root declaration.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RevisionManifestRootDeclare {
|
||||
pub(crate) root_id: ExGuid,
|
||||
pub(crate) object_id: ExGuid,
|
||||
}
|
||||
|
||||
impl RevisionManifestRootDeclare {
|
||||
fn parse(reader: Reader) -> Result<RevisionManifestRootDeclare> {
|
||||
let root_id = ExGuid::parse(reader)?;
|
||||
let object_id = ExGuid::parse(reader)?;
|
||||
|
||||
Ok(RevisionManifestRootDeclare { root_id, object_id })
|
||||
}
|
||||
}
|
||||
|
||||
impl DataElement {
|
||||
pub(crate) fn parse_revision_manifest(reader: Reader) -> Result<RevisionManifest> {
|
||||
ObjectHeader::try_parse_16(reader, ObjectType::RevisionManifest)?;
|
||||
|
||||
let rev_id = ExGuid::parse(reader)?;
|
||||
let base_rev_id = ExGuid::parse(reader)?;
|
||||
|
||||
let mut root_declare = vec![];
|
||||
let mut group_references = vec![];
|
||||
|
||||
loop {
|
||||
if ObjectHeader::has_end_8(reader, ObjectType::DataElement)? {
|
||||
break;
|
||||
}
|
||||
|
||||
let object_header = ObjectHeader::parse_16(reader)?;
|
||||
|
||||
match object_header.object_type {
|
||||
ObjectType::RevisionManifestRoot => {
|
||||
root_declare.push(RevisionManifestRootDeclare::parse(reader)?)
|
||||
}
|
||||
ObjectType::RevisionManifestGroupReference => {
|
||||
group_references.push(ExGuid::parse(reader)?)
|
||||
}
|
||||
_ => {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("unexpected object type: {:x}", object_header.object_type).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
|
||||
|
||||
let manifest = RevisionManifest {
|
||||
rev_id,
|
||||
base_rev_id,
|
||||
root_declare,
|
||||
group_references,
|
||||
};
|
||||
|
||||
Ok(manifest)
|
||||
}
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::fsshttpb::data::cell_id::CellId;
|
||||
use crate::parser::fsshttpb::data::exguid::ExGuid;
|
||||
use crate::parser::fsshttpb::data::object_types::ObjectType;
|
||||
use crate::parser::fsshttpb::data::serial_number::SerialNumber;
|
||||
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
|
||||
use crate::parser::fsshttpb::data_element::DataElement;
|
||||
use crate::parser::Reader;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A storage index.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.2]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.2]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/f5724986-bd0f-488d-9b85-7d5f954d8e9a
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct StorageIndex {
|
||||
pub(crate) manifest_mappings: Vec<StorageIndexManifestMapping>,
|
||||
pub(crate) cell_mappings: HashMap<CellId, StorageIndexCellMapping>,
|
||||
pub(crate) revision_mappings: HashMap<ExGuid, StorageIndexRevisionMapping>,
|
||||
}
|
||||
|
||||
impl StorageIndex {
|
||||
pub(crate) fn find_cell_mapping_id(&self, cell_id: CellId) -> Option<ExGuid> {
|
||||
self.cell_mappings.get(&cell_id).map(|mapping| mapping.id)
|
||||
}
|
||||
|
||||
pub(crate) fn find_revision_mapping_id(&self, id: ExGuid) -> Option<ExGuid> {
|
||||
self.revision_mappings
|
||||
.get(&id)
|
||||
.map(|mapping| mapping.revision_mapping)
|
||||
}
|
||||
}
|
||||
|
||||
/// A storage indexes manifest mapping.
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct StorageIndexManifestMapping {
|
||||
pub(crate) mapping_id: ExGuid,
|
||||
pub(crate) serial: SerialNumber,
|
||||
}
|
||||
|
||||
/// A storage indexes cell mapping.
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct StorageIndexCellMapping {
|
||||
pub(crate) cell_id: CellId,
|
||||
pub(crate) id: ExGuid,
|
||||
pub(crate) serial: SerialNumber,
|
||||
}
|
||||
|
||||
/// A storage indexes revision mapping.
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct StorageIndexRevisionMapping {
|
||||
pub(crate) revision_mapping: ExGuid,
|
||||
pub(crate) serial: SerialNumber,
|
||||
}
|
||||
|
||||
impl DataElement {
|
||||
pub(crate) fn parse_storage_index(reader: Reader) -> Result<StorageIndex> {
|
||||
let mut manifest_mappings = vec![];
|
||||
let mut cell_mappings = HashMap::new();
|
||||
let mut revision_mappings = HashMap::new();
|
||||
|
||||
loop {
|
||||
if ObjectHeader::has_end_8(reader, ObjectType::DataElement)? {
|
||||
break;
|
||||
}
|
||||
|
||||
let object_header = ObjectHeader::parse_16(reader)?;
|
||||
match object_header.object_type {
|
||||
ObjectType::StorageIndexManifestMapping => {
|
||||
manifest_mappings.push(Self::parse_storage_index_manifest_mapping(reader)?)
|
||||
}
|
||||
ObjectType::StorageIndexCellMapping => {
|
||||
let (id, mapping) = Self::parse_storage_index_cell_mapping(reader)?;
|
||||
|
||||
cell_mappings.insert(id, mapping);
|
||||
}
|
||||
ObjectType::StorageIndexRevisionMapping => {
|
||||
let (id, mapping) = Self::parse_storage_index_revision_mapping(reader)?;
|
||||
|
||||
revision_mappings.insert(id, mapping);
|
||||
}
|
||||
_ => {
|
||||
return Err(ErrorKind::MalformedFssHttpBData(
|
||||
format!("unexpected object type: {:x}", object_header.object_type).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
|
||||
|
||||
Ok(StorageIndex {
|
||||
manifest_mappings,
|
||||
cell_mappings,
|
||||
revision_mappings,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_storage_index_manifest_mapping(reader: Reader) -> Result<StorageIndexManifestMapping> {
|
||||
let mapping_id = ExGuid::parse(reader)?;
|
||||
let serial = SerialNumber::parse(reader)?;
|
||||
|
||||
Ok(StorageIndexManifestMapping { mapping_id, serial })
|
||||
}
|
||||
|
||||
fn parse_storage_index_cell_mapping(
|
||||
reader: Reader,
|
||||
) -> Result<(CellId, StorageIndexCellMapping)> {
|
||||
let cell_id = CellId::parse(reader)?;
|
||||
let id = ExGuid::parse(reader)?;
|
||||
let serial = SerialNumber::parse(reader)?;
|
||||
|
||||
let mapping = StorageIndexCellMapping {
|
||||
cell_id,
|
||||
id,
|
||||
serial,
|
||||
};
|
||||
|
||||
Ok((cell_id, mapping))
|
||||
}
|
||||
|
||||
fn parse_storage_index_revision_mapping(
|
||||
reader: Reader,
|
||||
) -> Result<(ExGuid, StorageIndexRevisionMapping)> {
|
||||
let id = ExGuid::parse(reader)?;
|
||||
let revision_mapping = ExGuid::parse(reader)?;
|
||||
let serial = SerialNumber::parse(reader)?;
|
||||
|
||||
let mapping = StorageIndexRevisionMapping {
|
||||
revision_mapping,
|
||||
serial,
|
||||
};
|
||||
|
||||
Ok((id, mapping))
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
use crate::parser::errors::Result;
|
||||
use crate::parser::fsshttpb::data::cell_id::CellId;
|
||||
use crate::parser::fsshttpb::data::exguid::ExGuid;
|
||||
use crate::parser::fsshttpb::data::object_types::ObjectType;
|
||||
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
|
||||
use crate::parser::fsshttpb::data_element::DataElement;
|
||||
use crate::parser::shared::guid::Guid;
|
||||
use crate::parser::Reader;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A storage manifest.
|
||||
///
|
||||
/// See [\[MS-FSSHTTPB\] 2.2.1.12.3]
|
||||
///
|
||||
/// [\[MS-FSSHTTPB\] 2.2.1.12.3]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/a681199b-45f3-4378-b929-fb13e674ac5c
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct StorageManifest {
|
||||
pub(crate) id: Guid,
|
||||
pub(crate) roots: HashMap<ExGuid, CellId>,
|
||||
}
|
||||
|
||||
impl DataElement {
|
||||
pub(crate) fn parse_storage_manifest(reader: Reader) -> Result<StorageManifest> {
|
||||
ObjectHeader::try_parse_16(reader, ObjectType::StorageManifest)?;
|
||||
|
||||
let id = Guid::parse(reader)?;
|
||||
|
||||
let mut roots = HashMap::new();
|
||||
|
||||
loop {
|
||||
if ObjectHeader::has_end_8(reader, ObjectType::DataElement)? {
|
||||
break;
|
||||
}
|
||||
|
||||
ObjectHeader::try_parse_16(reader, ObjectType::StorageManifestRoot)?;
|
||||
|
||||
let root_manifest = ExGuid::parse(reader)?;
|
||||
let cell = CellId::parse(reader)?;
|
||||
|
||||
roots.insert(root_manifest, cell);
|
||||
}
|
||||
|
||||
ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?;
|
||||
|
||||
Ok(StorageManifest { id, roots })
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
//! The FSSHTTP binary packaging format.
|
||||
//!
|
||||
//! This is the lowest level of the OneNote file format as the FSSHTTPB format specifies how
|
||||
//! objects and revisions are stored in a binary file.
|
||||
//!
|
||||
//! See [\[MS-FSSHTTPB\]]
|
||||
//!
|
||||
//! [\[MS-FSSHTTPB\]]: https://docs.microsoft.com/en-us/openspecs/sharepoint_protocols/ms-fsshttpb/f59fc37d-2232-4b14-baac-25f98e9e7b5a
|
||||
|
||||
pub(crate) mod data;
|
||||
pub(crate) mod data_element;
|
||||
pub(crate) mod packaging;
|
@ -1,62 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::fsshttpb::data::exguid::ExGuid;
|
||||
use crate::parser::fsshttpb::data::object_types::ObjectType;
|
||||
use crate::parser::fsshttpb::data::stream_object::ObjectHeader;
|
||||
use crate::parser::fsshttpb::data_element::DataElementPackage;
|
||||
use crate::parser::shared::guid::Guid;
|
||||
use crate::parser::Reader;
|
||||
|
||||
/// A OneNote file packaged in FSSHTTPB format.
|
||||
///
|
||||
/// See [\[MS-ONESTORE\] 2.8.1]
|
||||
///
|
||||
/// [\[MS-ONESTORE\] 2.8.1]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-onestore/a2f046ea-109a-49c4-912d-dc2888cf0565
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct OneStorePackaging {
|
||||
pub(crate) file_type: Guid,
|
||||
pub(crate) file: Guid,
|
||||
pub(crate) legacy_file_version: Guid,
|
||||
pub(crate) file_format: Guid,
|
||||
pub(crate) storage_index: ExGuid,
|
||||
pub(crate) cell_schema: Guid,
|
||||
pub(crate) data_element_package: DataElementPackage,
|
||||
}
|
||||
|
||||
impl OneStorePackaging {
|
||||
pub(crate) fn parse(reader: Reader) -> Result<OneStorePackaging> {
|
||||
let file_type = Guid::parse(reader)?;
|
||||
let file = Guid::parse(reader)?;
|
||||
let legacy_file_version = Guid::parse(reader)?;
|
||||
let file_format = Guid::parse(reader)?;
|
||||
|
||||
if file != legacy_file_version {
|
||||
return Err(
|
||||
ErrorKind::MalformedOneStoreData("not a legacy OneStore file".into()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
if reader.get_u32()? != 0 {
|
||||
return Err(ErrorKind::MalformedFssHttpBData("invalid padding data".into()).into());
|
||||
}
|
||||
|
||||
ObjectHeader::try_parse_32(reader, ObjectType::OneNotePackaging)?;
|
||||
|
||||
let storage_index = ExGuid::parse(reader)?;
|
||||
let cell_schema = Guid::parse(reader)?;
|
||||
|
||||
let data_element_package = DataElementPackage::parse(reader)?;
|
||||
|
||||
ObjectHeader::try_parse_end_16(reader, ObjectType::OneNotePackaging)?;
|
||||
|
||||
Ok(OneStorePackaging {
|
||||
file_type,
|
||||
file,
|
||||
legacy_file_version,
|
||||
file_format,
|
||||
storage_index,
|
||||
cell_schema,
|
||||
data_element_package,
|
||||
})
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
macro_rules! guid {
|
||||
({ $p0:tt - $p1:tt - $p2:tt - $p3:tt - $p4:tt }) => {
|
||||
crate::parser::shared::guid::Guid::from_str(concat!(
|
||||
stringify!($p0),
|
||||
'-',
|
||||
stringify!($p1),
|
||||
'-',
|
||||
stringify!($p2),
|
||||
'-',
|
||||
stringify!($p3),
|
||||
'-',
|
||||
stringify!($p4),
|
||||
))
|
||||
.unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! exguid {
|
||||
({$guid:tt , $n:literal}) => {
|
||||
crate::parser::fsshttpb::data::exguid::ExGuid::from_guid(guid!($guid), $n)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::parser::fsshttpb::data::exguid::ExGuid;
|
||||
use crate::parser::shared::guid::Guid;
|
||||
|
||||
#[test]
|
||||
fn parse_guid() {
|
||||
let guid = guid!({ 1A5A319C - C26B - 41AA - B9C5 - 9BD8C44E07D4 });
|
||||
|
||||
assert_eq!(
|
||||
guid,
|
||||
Guid::from_str("1A5A319C-C26B-41AA-B9C5-9BD8C44E07D4").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_exguid() {
|
||||
let guid = exguid!({{1A5A319C-C26B-41AA-B9C5-9BD8C44E07D4}, 1});
|
||||
|
||||
assert_eq!(
|
||||
guid,
|
||||
ExGuid::from_guid(
|
||||
Guid::from_str("1A5A319C-C26B-41AA-B9C5-9BD8C44E07D4").unwrap(),
|
||||
1
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
//! A OneNote file parser.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unused_must_use)]
|
||||
pub mod errors;
|
||||
mod fsshttpb;
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod one;
|
||||
mod onenote;
|
||||
mod onestore;
|
||||
mod reader;
|
||||
mod shared;
|
||||
mod utils;
|
||||
|
||||
pub(crate) type Reader<'a, 'b> = &'b mut crate::parser::reader::Reader<'a>;
|
||||
|
||||
pub use onenote::Parser;
|
||||
|
||||
/// The data that represents a OneNote notebook.
|
||||
pub mod notebook {
|
||||
pub use crate::parser::onenote::notebook::Notebook;
|
||||
}
|
||||
|
||||
/// The data that represents a OneNote section.
|
||||
pub mod section {
|
||||
pub use crate::parser::onenote::section::{Section, SectionEntry};
|
||||
}
|
||||
|
||||
/// The data that represents a OneNote page.
|
||||
pub mod page {
|
||||
pub use crate::parser::onenote::page::Page;
|
||||
pub use crate::parser::onenote::page_content::PageContent;
|
||||
}
|
||||
|
||||
/// The data that represents the contents of a OneNote section.
|
||||
pub mod contents {
|
||||
pub use crate::parser::onenote::content::Content;
|
||||
pub use crate::parser::onenote::embedded_file::EmbeddedFile;
|
||||
pub use crate::parser::onenote::image::Image;
|
||||
pub use crate::parser::onenote::ink::{Ink, InkBoundingBox, InkPoint, InkStroke};
|
||||
pub use crate::parser::onenote::list::List;
|
||||
pub use crate::parser::onenote::note_tag::NoteTag;
|
||||
pub use crate::parser::onenote::outline::{Outline, OutlineElement, OutlineItem};
|
||||
pub use crate::parser::onenote::rich_text::{EmbeddedObject, RichText};
|
||||
pub use crate::parser::onenote::table::{Table, TableCell};
|
||||
}
|
||||
|
||||
/// Collection of properties used by the OneNote file format.
|
||||
pub mod property {
|
||||
/// Properties related to multiple types of objects.
|
||||
pub mod common {
|
||||
pub use crate::parser::one::property::color::Color;
|
||||
pub use crate::parser::one::property::color_ref::ColorRef;
|
||||
}
|
||||
|
||||
/// Properties related to embedded files.
|
||||
pub mod embedded_file {
|
||||
pub use crate::parser::one::property::file_type::FileType;
|
||||
}
|
||||
|
||||
/// Properties related to note tags.
|
||||
pub mod note_tag {
|
||||
pub use crate::parser::one::property::note_tag::ActionItemStatus;
|
||||
pub use crate::parser::one::property::note_tag_shape::NoteTagShape;
|
||||
}
|
||||
|
||||
/// Properties related to rich-text content.
|
||||
pub mod rich_text {
|
||||
pub use crate::parser::one::property::paragraph_alignment::ParagraphAlignment;
|
||||
pub use crate::parser::onenote::rich_text::ParagraphStyling;
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
//! The OneNote file format.
|
||||
//!
|
||||
//! This module implements parsing OneNote objects from a OneNote revision store (see `onestore/`).
|
||||
//! It defines the types of objects we can parse along with their properties.
|
||||
//!
|
||||
//! See [\[MS-ONE\]]
|
||||
//!
|
||||
//! [\[MS-ONE\]]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/73d22548-a613-4350-8c23-07d15576be50
|
||||
|
||||
pub(crate) mod property;
|
||||
pub(crate) mod property_set;
|
@ -1,21 +0,0 @@
|
||||
use crate::parser::errors::Result;
|
||||
use crate::parser::one::property::{simple, PropertyType};
|
||||
use crate::parser::onestore::object::Object;
|
||||
|
||||
/// The author of an object.
|
||||
///
|
||||
/// See [\[MS-ONE\] 2.2.67]
|
||||
///
|
||||
/// [\[MS-ONE\] 2.2.67]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/db06251b-b672-4c9b-8ba5-d948caaa3edd
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Author(String);
|
||||
|
||||
impl Author {
|
||||
pub(crate) fn into_value(self) -> String {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub(crate) fn parse(object: &Object) -> Result<Option<Author>> {
|
||||
Ok(simple::parse_string(PropertyType::Author, object)?.map(Author))
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::one::property::PropertyType;
|
||||
use crate::parser::onestore::object::Object;
|
||||
|
||||
/// A charset representation.
|
||||
///
|
||||
/// See [\[MS-ONE\] 2.3.55].
|
||||
///
|
||||
/// [\[MS-ONE\] 2.3.55]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/64e2db6e-6eeb-443c-9ccf-0f72b37ba411
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Charset {
|
||||
Ansi,
|
||||
Default,
|
||||
Symbol,
|
||||
Mac,
|
||||
ShiftJis,
|
||||
Hangul,
|
||||
Johab,
|
||||
Gb2312,
|
||||
ChineseBig5,
|
||||
Greek,
|
||||
Turkish,
|
||||
Vietnamese,
|
||||
Hebrew,
|
||||
Arabic,
|
||||
Baltic,
|
||||
Russian,
|
||||
Thai,
|
||||
EastEurope,
|
||||
Oem,
|
||||
}
|
||||
|
||||
impl Charset {
|
||||
pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result<Option<Charset>> {
|
||||
let value = match object.props().get(prop_type) {
|
||||
Some(value) => value
|
||||
.to_u8()
|
||||
.ok_or_else(|| ErrorKind::MalformedOneNoteFileData("charset is not a u8".into()))?,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let charset = match value {
|
||||
0 => Charset::Ansi,
|
||||
1 => Charset::Default,
|
||||
2 => Charset::Symbol,
|
||||
77 => Charset::Mac,
|
||||
128 => Charset::ShiftJis,
|
||||
129 => Charset::Hangul,
|
||||
130 => Charset::Johab,
|
||||
134 => Charset::Gb2312,
|
||||
136 => Charset::ChineseBig5,
|
||||
161 => Charset::Greek,
|
||||
162 => Charset::Turkish,
|
||||
163 => Charset::Vietnamese,
|
||||
177 => Charset::Hebrew,
|
||||
178 => Charset::Arabic,
|
||||
186 => Charset::Baltic,
|
||||
204 => Charset::Russian,
|
||||
222 => Charset::Thai,
|
||||
238 => Charset::EastEurope,
|
||||
255 => Charset::Oem,
|
||||
_ => {
|
||||
return Err(ErrorKind::MalformedOneNoteFileData(
|
||||
format!("invalid charset: {}", value).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(charset))
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::one::property::PropertyType;
|
||||
use crate::parser::onestore::object::Object;
|
||||
|
||||
/// A RGBA color value.
|
||||
///
|
||||
/// See [\[MS-ONE\] 2.2.7]
|
||||
///
|
||||
/// [\[MS-ONE\] 2.2.7]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/6e4a87f9-18f0-4ad6-bc7d-0f326d61e136
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Color {
|
||||
alpha: u8,
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
}
|
||||
|
||||
impl Color {
|
||||
/// The color's transparency value.
|
||||
pub fn alpha(&self) -> u8 {
|
||||
self.alpha
|
||||
}
|
||||
|
||||
/// The color's red value.
|
||||
pub fn r(&self) -> u8 {
|
||||
self.r
|
||||
}
|
||||
|
||||
/// The color's green value.
|
||||
pub fn g(&self) -> u8 {
|
||||
self.g
|
||||
}
|
||||
|
||||
/// The color's blue value.
|
||||
pub fn b(&self) -> u8 {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result<Option<Color>> {
|
||||
let value = match object.props().get(prop_type) {
|
||||
Some(value) => value
|
||||
.to_u32()
|
||||
.ok_or_else(|| ErrorKind::MalformedOneNoteFileData("color is not a u32".into()))?,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let bytes = value.to_le_bytes();
|
||||
|
||||
Ok(Some(Color {
|
||||
alpha: 255 - bytes[3],
|
||||
r: bytes[0],
|
||||
g: bytes[1],
|
||||
b: bytes[2],
|
||||
}))
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::one::property::PropertyType;
|
||||
use crate::parser::onestore::object::Object;
|
||||
|
||||
/// An RGB color value.
|
||||
///
|
||||
/// See [\[MS-ONE\] 2.2.8]
|
||||
///
|
||||
/// [\[MS-ONE\] 2.2.8]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/3796cb27-7ec3-4dc9-b43e-7c31cc5b765d
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub enum ColorRef {
|
||||
/// Determined by the application.
|
||||
Auto,
|
||||
|
||||
/// A manually specified color
|
||||
Manual {
|
||||
/// The color's red value.
|
||||
r: u8,
|
||||
/// The color's green value.
|
||||
g: u8,
|
||||
/// The color's blue value
|
||||
b: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl ColorRef {
|
||||
pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result<Option<ColorRef>> {
|
||||
let value = match object.props().get(prop_type) {
|
||||
Some(value) => value.to_u32().ok_or_else(|| {
|
||||
ErrorKind::MalformedOneNoteFileData("color ref is not a u32".into())
|
||||
})?,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let bytes = value.to_le_bytes();
|
||||
|
||||
let color = match bytes[3] {
|
||||
0xFF => ColorRef::Auto,
|
||||
0x00 => ColorRef::Manual {
|
||||
r: bytes[0],
|
||||
g: bytes[1],
|
||||
b: bytes[2],
|
||||
},
|
||||
_ => {
|
||||
return Err(ErrorKind::MalformedOneNoteFileData(
|
||||
format!("invalid color ref: 0x{:08X}", value).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Some(color))
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
use crate::parser::errors::{ErrorKind, Result};
|
||||
use crate::parser::one::property::PropertyType;
|
||||
use crate::parser::onestore::object::Object;
|
||||
|
||||
/// An embedded file's file type.
|
||||
///
|
||||
/// See [\[MS-ONE\] 2.3.62].
|
||||
///
|
||||
/// [\[MS-ONE\] 2.3.62]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/112836a0-ed3b-4be1-bc4b-49f0f7b02295
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub enum FileType {
|
||||
/// Unknown
|
||||
Unknown,
|
||||
|
||||
/// An audio file.
|
||||
Audio,
|
||||
|
||||
/// A video file.
|
||||
Video,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub(crate) fn parse(object: &Object) -> Result<FileType> {
|
||||
let value = match object.props().get(PropertyType::IRecordMedia) {
|
||||
Some(value) => value.to_u32().ok_or_else(|| {
|
||||
ErrorKind::MalformedOneNoteFileData("file type status is not a u32".into())
|
||||
})?,
|
||||
None => return Ok(FileType::Unknown),
|
||||
};
|
||||
|
||||
let file_type = match value {
|
||||
1 => FileType::Audio,
|
||||
2 => FileType::Video,
|
||||
_ => {
|
||||
return Err(ErrorKind::MalformedOneNoteFileData(
|
||||
format!("invalid file type: {}", value).into(),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(file_type)
|
||||
}
|
||||
}
|