Desktop: Add OneNote Importer (#10642)
@ -85,6 +85,7 @@ plugin_types/
|
|||||||
readme/
|
readme/
|
||||||
packages/react-native-vosk/lib/
|
packages/react-native-vosk/lib/
|
||||||
packages/lib/countable/Countable.js
|
packages/lib/countable/Countable.js
|
||||||
|
packages/onenote-converter/pkg/onenote_converter.js
|
||||||
|
|
||||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||||
packages/app-cli/app/LinkSelector.js
|
packages/app-cli/app/LinkSelector.js
|
||||||
@ -1172,6 +1173,8 @@ packages/lib/services/interop/InteropService_Importer_Md.test.js
|
|||||||
packages/lib/services/interop/InteropService_Importer_Md.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.test.js
|
||||||
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.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.test.js
|
||||||
packages/lib/services/interop/InteropService_Importer_Raw.js
|
packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||||
packages/lib/services/interop/Module.test.js
|
packages/lib/services/interop/Module.test.js
|
||||||
|
1
.github/scripts/run_ci.sh
vendored
@ -67,6 +67,7 @@ echo "IS_MACOS=$IS_MACOS"
|
|||||||
echo "Node $( node -v )"
|
echo "Node $( node -v )"
|
||||||
echo "Npm $( npm -v )"
|
echo "Npm $( npm -v )"
|
||||||
echo "Yarn $( yarn -v )"
|
echo "Yarn $( yarn -v )"
|
||||||
|
echo "Rust $( rustc --version )"
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Install packages
|
# Install packages
|
||||||
|
2
.github/workflows/build-android.yml
vendored
@ -26,6 +26,8 @@ jobs:
|
|||||||
node-version: '18'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
- name: Install Yarn
|
- name: Install Yarn
|
||||||
run: |
|
run: |
|
||||||
corepack enable
|
corepack enable
|
||||||
|
1
.github/workflows/github-actions-main.yml
vendored
@ -69,6 +69,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: olegtarasov/get-tag@v2.1.3
|
- uses: olegtarasov/get-tag@v2.1.3
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||||
|
2
.gitignore
vendored
@ -1149,6 +1149,8 @@ packages/lib/services/interop/InteropService_Importer_Md.test.js
|
|||||||
packages/lib/services/interop/InteropService_Importer_Md.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.test.js
|
||||||
packages/lib/services/interop/InteropService_Importer_Md_frontmatter.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.test.js
|
||||||
packages/lib/services/interop/InteropService_Importer_Raw.js
|
packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||||
packages/lib/services/interop/Module.test.js
|
packages/lib/services/interop/Module.test.js
|
||||||
|
@ -35,6 +35,9 @@ COPY packages/utils ./packages/utils
|
|||||||
COPY packages/lib ./packages/lib
|
COPY packages/lib ./packages/lib
|
||||||
COPY packages/server ./packages/server
|
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
|
# 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
|
# 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
|
# but we can delete it anyway. We can delete the cache because we use
|
||||||
|
@ -5,6 +5,9 @@
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"rust-analyzer.linkedProjects": [
|
||||||
|
"./packages/onenote-converter/Cargo.toml",
|
||||||
|
],
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"_mydocs/mdtest/": true,
|
"_mydocs/mdtest/": true,
|
||||||
"_releases/": true,
|
"_releases/": true,
|
||||||
|
BIN
packages/app-cli/tests/support/onenote/bug_broken_character.zip
Normal file
BIN
packages/app-cli/tests/support/onenote/complex_notes.zip
Normal file
BIN
packages/app-cli/tests/support/onenote/group_sections.zip
Normal file
42
packages/app-cli/tests/support/onenote/many_svgs.html
Normal file
BIN
packages/app-cli/tests/support/onenote/simple_notebook.zip
Normal file
BIN
packages/app-cli/tests/support/onenote/subpages.zip
Normal file
BIN
packages/app-cli/tests/support/onenote/subsections.zip
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!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,6 +25,7 @@ const { ResourceScreen } = require('./ResourceScreen.js');
|
|||||||
import Navigator from './Navigator';
|
import Navigator from './Navigator';
|
||||||
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
import WelcomeUtils from '@joplin/lib/WelcomeUtils';
|
||||||
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
|
import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
|
||||||
|
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||||
import WindowCommandsAndDialogs from './WindowCommandsAndDialogs/WindowCommandsAndDialogs';
|
import WindowCommandsAndDialogs from './WindowCommandsAndDialogs/WindowCommandsAndDialogs';
|
||||||
import { defaultWindowId, stateUtils, WindowState } from '@joplin/lib/reducer';
|
import { defaultWindowId, stateUtils, WindowState } from '@joplin/lib/reducer';
|
||||||
import bridge from '../services/bridge';
|
import bridge from '../services/bridge';
|
||||||
@ -91,6 +92,9 @@ async function initialize() {
|
|||||||
type: 'NOTE_VISIBLE_PANES_SET',
|
type: 'NOTE_VISIBLE_PANES_SET',
|
||||||
panes: Setting.value('noteVisiblePanes'),
|
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
|
// 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 paginationToSql from './models/utils/paginationToSql';
|
||||||
import Database from './database';
|
import Database from './database';
|
||||||
import uuid from './uuid';
|
|
||||||
import time from './time';
|
import time from './time';
|
||||||
import JoplinDatabase, { TableField } from './JoplinDatabase';
|
import JoplinDatabase, { TableField } from './JoplinDatabase';
|
||||||
import { LoadOptions, SaveOptions } from './models/utils/types';
|
import { LoadOptions, SaveOptions } from './models/utils/types';
|
||||||
import ActionLogger, { ItemActionType as ItemActionType } from './utils/ActionLogger';
|
import ActionLogger, { ItemActionType as ItemActionType } from './utils/ActionLogger';
|
||||||
import { BaseItemEntity, SqlQuery } from './services/database/types';
|
import { BaseItemEntity, SqlQuery } from './services/database/types';
|
||||||
|
import uuid from './uuid';
|
||||||
const Mutex = require('async-mutex').Mutex;
|
const Mutex = require('async-mutex').Mutex;
|
||||||
|
|
||||||
// New code should make use of this enum
|
// New code should make use of this enum
|
||||||
@ -80,6 +80,8 @@ class BaseModel {
|
|||||||
['TYPE_COMMAND', ModelType.Command],
|
['TYPE_COMMAND', ModelType.Command],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static uuidGenerator: ()=> string = uuid.create;
|
||||||
|
|
||||||
public static TYPE_NOTE = ModelType.Note;
|
public static TYPE_NOTE = ModelType.Note;
|
||||||
public static TYPE_FOLDER = ModelType.Folder;
|
public static TYPE_FOLDER = ModelType.Folder;
|
||||||
public static TYPE_SETTING = ModelType.Setting;
|
public static TYPE_SETTING = ModelType.Setting;
|
||||||
@ -576,7 +578,7 @@ class BaseModel {
|
|||||||
|
|
||||||
if (options.isNew) {
|
if (options.isNew) {
|
||||||
if (this.useUuid() && !o.id) {
|
if (this.useUuid() && !o.id) {
|
||||||
modelId = uuid.create();
|
modelId = this.generateUuid();
|
||||||
o.id = modelId;
|
o.id = modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -757,6 +759,15 @@ class BaseModel {
|
|||||||
return this.db_;
|
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() {
|
// static isReady() {
|
||||||
// return !!this.db_;
|
// return !!this.db_;
|
||||||
// }
|
// }
|
||||||
|
@ -17,9 +17,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/react-hooks": "8.0.1",
|
"@testing-library/react-hooks": "8.0.1",
|
||||||
|
"@types/adm-zip": "0.5.5",
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"@types/jest": "29.5.12",
|
"@types/jest": "29.5.12",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
|
"@types/jsdom": "21.1.6",
|
||||||
"@types/markdown-it": "13.0.9",
|
"@types/markdown-it": "13.0.9",
|
||||||
"@types/mustache": "4.2.5",
|
"@types/mustache": "4.2.5",
|
||||||
"@types/node": "18.19.42",
|
"@types/node": "18.19.42",
|
||||||
@ -29,6 +31,7 @@
|
|||||||
"canvas": "2.11.2",
|
"canvas": "2.11.2",
|
||||||
"clean-html": "1.5.0",
|
"clean-html": "1.5.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
|
"jsdom": "23.2.0",
|
||||||
"pdfjs-dist": "3.11.174",
|
"pdfjs-dist": "3.11.174",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-test-renderer": "18.3.1",
|
"react-test-renderer": "18.3.1",
|
||||||
@ -44,11 +47,13 @@
|
|||||||
"@joplin/fork-sax": "^1.2.56",
|
"@joplin/fork-sax": "^1.2.56",
|
||||||
"@joplin/fork-uslug": "^1.0.17",
|
"@joplin/fork-uslug": "^1.0.17",
|
||||||
"@joplin/htmlpack": "~3.2",
|
"@joplin/htmlpack": "~3.2",
|
||||||
|
"@joplin/onenote-converter": "0.0.1",
|
||||||
"@joplin/renderer": "~3.2",
|
"@joplin/renderer": "~3.2",
|
||||||
"@joplin/turndown": "^4.0.74",
|
"@joplin/turndown": "^4.0.74",
|
||||||
"@joplin/turndown-plugin-gfm": "^1.0.56",
|
"@joplin/turndown-plugin-gfm": "^1.0.56",
|
||||||
"@joplin/utils": "~3.2",
|
"@joplin/utils": "~3.2",
|
||||||
"@types/nanoid": "3.0.0",
|
"@types/nanoid": "3.0.0",
|
||||||
|
"adm-zip": "0.5.12",
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"base-64": "1.0.0",
|
"base-64": "1.0.0",
|
||||||
"base64-stream": "1.0.0",
|
"base64-stream": "1.0.0",
|
||||||
|
@ -30,6 +30,8 @@ export default class InteropService {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
private eventEmitter_: any = null;
|
private eventEmitter_: any = null;
|
||||||
private static instance_: InteropService;
|
private static instance_: InteropService;
|
||||||
|
private document_: Document;
|
||||||
|
private xmlSerializer_: XMLSerializer;
|
||||||
|
|
||||||
public static instance(): InteropService {
|
public static instance(): InteropService {
|
||||||
if (!this.instance_) this.instance_ = new InteropService();
|
if (!this.instance_) this.instance_ = new InteropService();
|
||||||
@ -133,6 +135,14 @@ export default class InteropService {
|
|||||||
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
|
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
|
||||||
description: _('Text document'),
|
description: _('Text document'),
|
||||||
}, () => new InteropService_Importer_Md()),
|
}, () => 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 = [
|
const exportModules = [
|
||||||
@ -189,6 +199,22 @@ export default class InteropService {
|
|||||||
this.eventEmitter_.emit('modulesChanged');
|
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")
|
// Find the module that matches the given type ("importer" or "exporter")
|
||||||
// and the given format. Some formats can have multiple associated importers
|
// and the given format. Some formats can have multiple associated importers
|
||||||
// or exporters, such as ENEX. In this case, the one marked as "isDefault"
|
// or exporters, such as ENEX. In this case, the one marked as "isDefault"
|
||||||
@ -273,6 +299,8 @@ export default class InteropService {
|
|||||||
format: 'auto',
|
format: 'auto',
|
||||||
destinationFolderId: null,
|
destinationFolderId: null,
|
||||||
destinationFolder: null,
|
destinationFolder: null,
|
||||||
|
xmlSerializer: this.xmlSerializer,
|
||||||
|
document: this.document,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,176 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
157
packages/lib/services/interop/InteropService_Importer_OneNote.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
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,6 +51,8 @@ export interface ImportOptions {
|
|||||||
onProgress?: (progressState: any, progress?: any)=> void;
|
onProgress?: (progressState: any, progress?: any)=> void;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
onError?: (error: any)=> void;
|
onError?: (error: any)=> void;
|
||||||
|
document?: Document;
|
||||||
|
xmlSerializer?: XMLSerializer;
|
||||||
|
|
||||||
defaultFolderTitle?: string;
|
defaultFolderTitle?: string;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import crypto from './services/e2ee/crypto';
|
|||||||
|
|
||||||
import FileApiDriverLocal from './file-api-driver-local';
|
import FileApiDriverLocal from './file-api-driver-local';
|
||||||
import * as mimeUtils from './mime-utils';
|
import * as mimeUtils from './mime-utils';
|
||||||
|
import BaseItem from './models/BaseItem';
|
||||||
const { _ } = require('./locale');
|
const { _ } = require('./locale');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
@ -309,13 +310,11 @@ function shimInit(options: ShimInitOptions = null) {
|
|||||||
|
|
||||||
const isUpdate = !!options.destinationResourceId;
|
const isUpdate = !!options.destinationResourceId;
|
||||||
|
|
||||||
const uuid = require('./uuid').default;
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(filePath))) throw new Error(_('Cannot access %s', filePath));
|
if (!(await fs.pathExists(filePath))) throw new Error(_('Cannot access %s', filePath));
|
||||||
|
|
||||||
defaultProps = defaultProps ? defaultProps : {};
|
defaultProps = defaultProps ? defaultProps : {};
|
||||||
|
|
||||||
let resourceId = defaultProps.id ? defaultProps.id : uuid.create();
|
let resourceId = defaultProps.id ? defaultProps.id : BaseItem.generateUuid();
|
||||||
if (isUpdate) resourceId = options.destinationResourceId;
|
if (isUpdate) resourceId = options.destinationResourceId;
|
||||||
|
|
||||||
let resource = isUpdate ? {} : Resource.new();
|
let resource = isUpdate ? {} : Resource.new();
|
||||||
|
7
packages/onenote-converter/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/target
|
||||||
|
/output
|
||||||
|
|
||||||
|
/.idea
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
/pkg
|
5
packages/onenote-converter/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"rust-analyzer.linkedProjects": [
|
||||||
|
"./Cargo.toml"
|
||||||
|
]
|
||||||
|
}
|
1040
packages/onenote-converter/Cargo.lock
generated
Normal file
41
packages/onenote-converter/Cargo.toml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
[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"]
|
66
packages/onenote-converter/README.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# 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).
|
2
packages/onenote-converter/askama.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[general]
|
||||||
|
dirs = ["src/templates"]
|
201
packages/onenote-converter/assets/icons/License
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
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.
|
6
packages/onenote-converter/assets/icons/arrow-right-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 234 B |
6
packages/onenote-converter/assets/icons/award-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 368 B |
6
packages/onenote-converter/assets/icons/book-open-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 340 B |
6
packages/onenote-converter/assets/icons/chat-4-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 256 B |
6
packages/onenote-converter/assets/icons/check-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 218 B |
6
packages/onenote-converter/assets/icons/checkbox-blank-circle-fill.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 172 B |
6
packages/onenote-converter/assets/icons/checkbox-blank-circle-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 260 B |
6
packages/onenote-converter/assets/icons/checkbox-blank-fill.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 223 B |
6
packages/onenote-converter/assets/icons/checkbox-blank-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 238 B |
6
packages/onenote-converter/assets/icons/checkbox-fill.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 302 B |
6
packages/onenote-converter/assets/icons/contacts-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 358 B |
6
packages/onenote-converter/assets/icons/error-warning-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 290 B |
6
packages/onenote-converter/assets/icons/file-list-2-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 282 B |
6
packages/onenote-converter/assets/icons/film-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 404 B |
6
packages/onenote-converter/assets/icons/flag-fill.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 261 B |
6
packages/onenote-converter/assets/icons/home-4-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 279 B |
6
packages/onenote-converter/assets/icons/lightbulb-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 457 B |
6
packages/onenote-converter/assets/icons/link.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 464 B |
6
packages/onenote-converter/assets/icons/lock-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 303 B |
6
packages/onenote-converter/assets/icons/mark-pen-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 555 B |
6
packages/onenote-converter/assets/icons/music-fill.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 193 B |
6
packages/onenote-converter/assets/icons/phone-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 802 B |
6
packages/onenote-converter/assets/icons/question-mark.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 432 B |
6
packages/onenote-converter/assets/icons/send-plane-2-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 295 B |
1
packages/onenote-converter/assets/icons/star-fill.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 246 B |
6
packages/onenote-converter/assets/icons/user-line.svg
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 312 B |
23
packages/onenote-converter/build.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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);
|
||||||
|
});
|
22
packages/onenote-converter/deny.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[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"
|
49
packages/onenote-converter/node_functions.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
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,
|
||||||
|
};
|
28
packages/onenote-converter/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
87
packages/onenote-converter/src/lib.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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(())
|
||||||
|
}
|
114
packages/onenote-converter/src/notebook.rs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
22
packages/onenote-converter/src/page/content.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
packages/onenote-converter/src/page/embedded_file.rs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
packages/onenote-converter/src/page/image.rs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
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!()
|
||||||
|
}
|
||||||
|
}
|
210
packages/onenote-converter/src/page/ink.rs
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
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())
|
||||||
|
}
|
182
packages/onenote-converter/src/page/list.rs
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
100
packages/onenote-converter/src/page/mod.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
539
packages/onenote-converter/src/page/note_tag.rs
Normal file
@ -0,0 +1,539 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
146
packages/onenote-converter/src/page/outline.rs
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
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(),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
306
packages/onenote-converter/src/page/rich_text.rs
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
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()
|
||||||
|
}
|
121
packages/onenote-converter/src/page/table.rs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
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()
|
||||||
|
}
|
123
packages/onenote-converter/src/parser/errors.rs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
//! 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,
|
||||||
|
},
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,195 @@
|
|||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
118
packages/onenote-converter/src/parser/fsshttpb/data/exguid.rs
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
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;
|
@ -0,0 +1,51 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,234 @@
|
|||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,196 @@
|
|||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
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()))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,336 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,142 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
12
packages/onenote-converter/src/parser/fsshttpb/mod.rs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
//! 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;
|
62
packages/onenote-converter/src/parser/fsshttpb/packaging.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
51
packages/onenote-converter/src/parser/macros.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
73
packages/onenote-converter/src/parser/mod.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
//! 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;
|
||||||
|
}
|
||||||
|
}
|
11
packages/onenote-converter/src/parser/one/mod.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
//! 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;
|
21
packages/onenote-converter/src/parser/one/property/author.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
58
packages/onenote-converter/src/parser/one/property/color.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|