diff --git a/.eslintignore b/.eslintignore index 4c6d7de0558..a5de229dd61 100644 --- a/.eslintignore +++ b/.eslintignore @@ -83,6 +83,7 @@ plugin_types/ readme/ packages/react-native-vosk/lib/ packages/lib/countable/Countable.js +packages/onenote-converter/pkg/onenote_converter.js # AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD packages/app-cli/app/LinkSelector.js @@ -1002,6 +1003,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_frontmatter.test.js packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js +packages/lib/services/interop/InteropService_Importer_OneNote.test.js +packages/lib/services/interop/InteropService_Importer_OneNote.js packages/lib/services/interop/InteropService_Importer_Raw.test.js packages/lib/services/interop/InteropService_Importer_Raw.js packages/lib/services/interop/Module.test.js diff --git a/.gitignore b/.gitignore index 3a8c8e28b62..c8ceb2c9218 100644 --- a/.gitignore +++ b/.gitignore @@ -982,6 +982,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_frontmatter.test.js packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js +packages/lib/services/interop/InteropService_Importer_OneNote.test.js +packages/lib/services/interop/InteropService_Importer_OneNote.js packages/lib/services/interop/InteropService_Importer_Raw.test.js packages/lib/services/interop/InteropService_Importer_Raw.js packages/lib/services/interop/Module.test.js diff --git a/Dockerfile.server b/Dockerfile.server index 5d0b7ab4b33..bff3b8eb471 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -33,6 +33,7 @@ COPY packages/renderer ./packages/renderer COPY packages/tools ./packages/tools COPY packages/utils ./packages/utils COPY packages/lib ./packages/lib +COPY packages/onenote-converter ./packages/onenote-converter COPY packages/server ./packages/server # For some reason there's both a .yarn/cache and .yarn/berry/cache that are diff --git a/joplin.code-workspace b/joplin.code-workspace index 57ed38a30db..e5bc8e5d896 100644 --- a/joplin.code-workspace +++ b/joplin.code-workspace @@ -5,6 +5,9 @@ }, ], "settings": { + "rust-analyzer.linkedProjects": [ + "./packages/onenote-converter/Cargo.toml", + ], "files.exclude": { "_mydocs/mdtest/": true, "_releases/": true, diff --git a/package.json b/package.json index c8d15952e66..cf297e34093 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "root", "private": true, "workspaces": [ + "packages/onenote-converter/pkg", "packages/*" ], "repository": { diff --git a/packages/app-cli/tests/support/onenote/complex_notes.zip b/packages/app-cli/tests/support/onenote/complex_notes.zip new file mode 100644 index 00000000000..d4ab2d717bc Binary files /dev/null and b/packages/app-cli/tests/support/onenote/complex_notes.zip differ diff --git a/packages/app-cli/tests/support/onenote/group_sections.zip b/packages/app-cli/tests/support/onenote/group_sections.zip new file mode 100644 index 00000000000..6abb5a2640c Binary files /dev/null and b/packages/app-cli/tests/support/onenote/group_sections.zip differ diff --git a/packages/app-cli/tests/support/onenote/simple_notebook.zip b/packages/app-cli/tests/support/onenote/simple_notebook.zip new file mode 100644 index 00000000000..bec29b3537a Binary files /dev/null and b/packages/app-cli/tests/support/onenote/simple_notebook.zip differ diff --git a/packages/app-cli/tests/support/onenote/subpages.zip b/packages/app-cli/tests/support/onenote/subpages.zip new file mode 100644 index 00000000000..dc2c054be01 Binary files /dev/null and b/packages/app-cli/tests/support/onenote/subpages.zip differ diff --git a/packages/app-cli/tests/support/onenote/subsections.zip b/packages/app-cli/tests/support/onenote/subsections.zip new file mode 100644 index 00000000000..624307c7861 Binary files /dev/null and b/packages/app-cli/tests/support/onenote/subsections.zip differ diff --git a/packages/lib/BaseModel.ts b/packages/lib/BaseModel.ts index ec5eda40a8f..2bfb0bdc678 100644 --- a/packages/lib/BaseModel.ts +++ b/packages/lib/BaseModel.ts @@ -1,11 +1,11 @@ import paginationToSql from './models/utils/paginationToSql'; import Database from './database'; -import uuid from './uuid'; import time from './time'; import JoplinDatabase, { TableField } from './JoplinDatabase'; import { LoadOptions, SaveOptions } from './models/utils/types'; import ActionLogger, { ItemActionType as ItemActionType } from './utils/ActionLogger'; import { SqlQuery } from './services/database/types'; +import uuid from './uuid'; const Mutex = require('async-mutex').Mutex; // New code should make use of this enum @@ -80,6 +80,8 @@ class BaseModel { ['TYPE_COMMAND', ModelType.Command], ]; + private static uuidGenerator: ()=> string = uuid.create; + public static TYPE_NOTE = ModelType.Note; public static TYPE_FOLDER = ModelType.Folder; public static TYPE_SETTING = ModelType.Setting; @@ -573,7 +575,7 @@ class BaseModel { if (options.isNew) { if (this.useUuid() && !o.id) { - modelId = uuid.create(); + modelId = this.generateUuid(); o.id = modelId; } @@ -754,6 +756,13 @@ class BaseModel { return this.db_; } + public static generateUuid() { + return this.uuidGenerator(); + } + + public static setIdGenerator(generator: ()=> string) { + this.uuidGenerator = generator; + } // static isReady() { // return !!this.db_; // } diff --git a/packages/lib/htmlUtils.ts b/packages/lib/htmlUtils.ts index 67fb11e32bf..7033f541598 100644 --- a/packages/lib/htmlUtils.ts +++ b/packages/lib/htmlUtils.ts @@ -172,7 +172,6 @@ class HtmlUtils { return output.join(' '); } - } export default new HtmlUtils(); diff --git a/packages/lib/package.json b/packages/lib/package.json index 8a2ad5315e6..31e058fc83f 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@testing-library/react-hooks": "8.0.1", + "@types/adm-zip": "0.5.5", "@types/fs-extra": "11.0.4", "@types/jest": "29.5.8", "@types/js-yaml": "4.0.9", @@ -44,11 +45,13 @@ "@joplin/fork-sax": "^1.2.55", "@joplin/fork-uslug": "^1.0.16", "@joplin/htmlpack": "~3.0", + "@joplin/onenote-converter": "0.0.1", "@joplin/renderer": "~3.0", "@joplin/turndown": "^4.0.73", "@joplin/turndown-plugin-gfm": "^1.0.55", "@joplin/utils": "~3.0", "@types/nanoid": "3.0.0", + "adm-zip": "0.5.12", "async-mutex": "0.4.1", "base-64": "1.0.0", "base64-stream": "1.0.0", diff --git a/packages/lib/services/interop/InteropService.ts b/packages/lib/services/interop/InteropService.ts index 9b76daaba1c..026887c22a2 100644 --- a/packages/lib/services/interop/InteropService.ts +++ b/packages/lib/services/interop/InteropService.ts @@ -19,6 +19,7 @@ import InteropService_Exporter_Md_frontmatter from './InteropService_Exporter_Md import InteropService_Importer_Base from './InteropService_Importer_Base'; import InteropService_Exporter_Base from './InteropService_Exporter_Base'; import Module, { dynamicRequireModuleFactory, makeExportModule, makeImportModule } from './Module'; +import InteropService_Importer_OneNote from './InteropService_Importer_OneNote'; const { sprintf } = require('sprintf-js'); const { fileExtension } = require('../../path-utils'); const EventEmitter = require('events'); @@ -133,6 +134,14 @@ export default class InteropService { isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format) description: _('Text document'), }, () => new InteropService_Importer_Md()), + + makeImportModule({ + format: 'zip', + fileExtensions: ['zip'], + sources: [FileSystemItem.File], + isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format) + description: _('OneNote Notebook'), + }, () => new InteropService_Importer_OneNote()), ]; const exportModules = [ diff --git a/packages/lib/services/interop/InteropService_Importer_Md.ts b/packages/lib/services/interop/InteropService_Importer_Md.ts index 22ef8cf1f0a..cc8fa470d35 100644 --- a/packages/lib/services/interop/InteropService_Importer_Md.ts +++ b/packages/lib/services/interop/InteropService_Importer_Md.ts @@ -12,7 +12,7 @@ import htmlUtils from '../../htmlUtils'; import { unique } from '../../ArrayUtils'; const { pregQuote } = require('../../string-utils-common'); import { MarkupToHtml } from '@joplin/renderer'; -import { isDataUrl } from '@joplin/utils/url'; +import { isDataUrl, isMailTo, isFilenameTooLong } from '@joplin/utils/url'; import { stripBom } from '../../string-utils'; export default class InteropService_Importer_Md extends InteropService_Importer_Base { @@ -108,11 +108,12 @@ export default class InteropService_Importer_Md extends InteropService_Importer_ let updated = md; const markdownLinks = markdownUtils.extractFileUrls(md); const htmlLinks = htmlUtils.extractFileUrls(md); - const fileLinks = unique(markdownLinks.concat(htmlLinks)); + const pdfLinks = htmlUtils.extractPdfUrls(md); + const fileLinks = unique(markdownLinks.concat(htmlLinks).concat(pdfLinks)); for (const encodedLink of fileLinks) { const link = decodeURI(encodedLink); - if (isDataUrl(link)) { + if (isDataUrl(link) || isMailTo(link) || isFilenameTooLong(link)) { // Just leave it as it is. We could potentially import // it as a resource but for now that's good enough. } else { diff --git a/packages/lib/services/interop/InteropService_Importer_OneNote.test.ts b/packages/lib/services/interop/InteropService_Importer_OneNote.test.ts new file mode 100644 index 00000000000..8d54019954d --- /dev/null +++ b/packages/lib/services/interop/InteropService_Importer_OneNote.test.ts @@ -0,0 +1,129 @@ +import Note from '../../models/Note'; +import Folder from '../../models/Folder'; +import * as fs from 'fs-extra'; +import { createTempDir, setupDatabaseAndSynchronizer, supportDir, switchClient } from '../../testing/test-utils'; +import { NoteEntity } from '../database/types'; +import InteropService_Importer_OneNote from './InteropService_Importer_OneNote'; +import { MarkupToHtml } from '@joplin/renderer'; +import BaseModel from '../../BaseModel'; +import uuid from '../../uuid'; + +describe('InteropService_Importer_OneNote', () => { + let tempDir: string; + async function importNote(path: string) { + const newFolder = await Folder.save({ title: 'folder' }); + const importer = new InteropService_Importer_OneNote(); + await importer.init(path, { + format: 'md', + outputFormat: 'md', + path, + destinationFolder: newFolder, + destinationFolderId: newFolder.id, + }); + importer.setMetadata({ fileExtensions: ['md'] }); + await importer.exec({ warnings: [] }); + const allNotes: NoteEntity[] = await Note.all(); + return allNotes; + } + beforeEach(async () => { + await setupDatabaseAndSynchronizer(1); + await switchClient(1); + tempDir = await createTempDir(); + }); + afterEach(async () => { + await fs.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('')[0]; + const menuLines = menuHtml.split(''); + + const pageTwo = notes.find(n => n.title === 'Page 2'); + expect(menuLines[3].trim()).toBe(`
  • ${pageTwo.title}`); + + const pageTwoA = notes.find(n => n.title === 'Page 2-a'); + expect(menuLines[4].trim()).toBe(`
  • ${pageTwoA.title}`); + + const pageTwoAA = notes.find(n => n.title === 'Page 2-a-a'); + expect(menuLines[5].trim()).toBe(`
  • ${pageTwoAA.title}`); + + const pageTwoB = notes.find(n => n.title === 'Page 2-b'); + expect(menuLines[7].trim()).toBe(`
  • ${pageTwoB.title}`); + }); + + 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; + BaseModel.setIdGenerator(() => String(idx++)); + const notes = await importNote(`${supportDir}/onenote/complex_notes.zip`); + + for (const note of notes) { + expect(note.body).toMatchSnapshot(note.title); + } + BaseModel.setIdGenerator(uuid.create); + }); + + 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); + }); +}); diff --git a/packages/lib/services/interop/InteropService_Importer_OneNote.ts b/packages/lib/services/interop/InteropService_Importer_OneNote.ts new file mode 100644 index 00000000000..9383443e211 --- /dev/null +++ b/packages/lib/services/interop/InteropService_Importer_OneNote.ts @@ -0,0 +1,70 @@ +import { ImportExportResult, ImportModuleOutputFormat } from './types'; + +import InteropService_Importer_Base from './InteropService_Importer_Base'; +import { NoteEntity } from '../database/types'; +import { rtrimSlashes } from '../../path-utils'; +import { oneNoteConverter } from '@joplin/onenote-converter'; +import * as AdmZip from 'adm-zip'; +import InteropService_Importer_Md from './InteropService_Importer_Md'; +import { join, resolve } from 'path'; +import Logger from '@joplin/utils/Logger'; +import path = require('path'); + +const logger = Logger.create('InteropService_Importer_OneNote'); + +export default class InteropService_Importer_OneNote extends InteropService_Importer_Base { + protected importedNotes: Record = {}; + + private getEntryDirectory(unzippedPath: string, entryName: string) { + const withoutBasePath = entryName.replace(unzippedPath, ''); + return path.normalize(withoutBasePath).split(path.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; + } + + // files that don't have a name seems to be local only and shouldn't be processed + + const tempOutputDirectory = await this.temporaryDirectory_(true); + const baseFolder = this.getEntryDirectory(unzipTempDirectory, files[0].entryName); + const notebookBaseDir = path.join(unzipTempDirectory, baseFolder, path.sep); + const outputDirectory2 = path.join(tempOutputDirectory, baseFolder); + + const notebookFiles = zip.getEntries().filter(e => e.name !== '.onetoc2' && e.name !== 'OneNote_RecycleBin.onetoc2'); + + 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('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); + + // remover temp directories? + return result; + } +} diff --git a/packages/lib/services/interop/__snapshots__/InteropService_Importer_OneNote.test.js.snap b/packages/lib/services/interop/__snapshots__/InteropService_Importer_OneNote.test.js.snap new file mode 100644 index 00000000000..57761e914f3 --- /dev/null +++ b/packages/lib/services/interop/__snapshots__/InteropService_Importer_OneNote.test.js.snap @@ -0,0 +1,403 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InteropService_Importer_OneNote should expect notes to be rendered the same: A page can have any width it wants 1`] = ` +" + + + + A page can have any width it wants? + + + + +
    A page can have any width it wants?
    +
    quinta-feira, 25 de abril de 2024
    +
    15:01
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget convallis velit. Nullam at luctus libero. Phasellus id pharetra odio. Duis luctus lorem ut tellus imperdiet, a aliquet elit pretium. Donec sit amet urna et mi gravida cursus et id felis. Ut quis congue velit, eget mollis tortor. Vestibulum porttitor lobortis justo, in imperdiet leo porta id. Sed ornare ex nisi, sed laoreet nulla suscipit a. Cras nec lectus porta, fermentum quam ac, sagittis ipsum. Ut massa lacus, ornare in hendrerit sit amet, tempor quis ligula. Nulla facilisi. Maecenas quam dolor, lacinia id magna nec, blandit tincidunt ipsum. Proin placerat dui gravida, lacinia tortor eu, rhoncus ex. Pellentesque accumsan nunc id venenatis condimentum. Aenean sodales tortor id risus varius, id tincidunt libero tincidunt. Curabitur quis interdum metus.

    +

     

    +

    This is another paragraph by the right side

    +
    + + + +" +`; + +exports[`InteropService_Importer_OneNote should expect notes to be rendered the same: A page with a lot of svgs 1`] = ` +" + + + + A page with a lot of svgs + + + + +
    A page with a lot of svgs
    +
    quinta-feira, 25 de abril de 2024
    +
    10:40
    +

    This is a text paragraph that should apppear behind the drawings

    +
    + + + +" +`; + +exports[`InteropService_Importer_OneNote should expect notes to be rendered the same: A page with text and drawing above it 1`] = ` +" + + + + A page with text and drawing above it + + + + +
    A page with text and drawing above it
    +
    quinta-feira, 25 de abril de 2024
    +
    02:48
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eget convallis velit. Nullam at luctus libero. Phasellus id pharetra odio. Duis luctus lorem ut tellus imperdiet, a aliquet elit pretium. Donec sit amet urna et mi gravida cursus et id felis. Ut quis congue velit, eget mollis tortor. Vestibulum porttitor lobortis justo, in imperdiet leo porta id. Sed ornare ex nisi, sed laoreet nulla suscipit a. Cras nec lectus porta, fermentum quam ac, sagittis ipsum. Ut massa lacus, ornare in hendrerit sit amet, tempor quis ligula. Nulla facilisi. Maecenas quam dolor, lacinia id magna nec, blandit tincidunt ipsum. Proin placerat dui gravida, lacinia tortor eu, rhoncus ex. Pellentesque accumsan nunc id venenatis condimentum. Aenean sodales tortor id risus varius, id tincidunt libero tincidunt. Curabitur quis interdum metus.

    +

    Vestibulum sed sem nec nulla tincidunt maximus. Nam nulla sapien, vestibulum ac eros a, eleifend sollicitudin lectus. Praesent pellentesque pulvinar porttitor. Morbi rutrum, erat nec blandit commodo, nunc nulla venenatis massa, at viverra leo nisi eu nisl. Sed cursus quam a sem mattis suscipit. Duis gravida tellus ut nibh congue aliquam. Nulla velit orci, pretium sed hendrerit a, vulputate in lacus. Sed vitae ligula ex.

    +

    Nullam ut ullamcorper arcu, a porta lectus. Nulla suscipit lorem et nibh viverra eleifend. Pellentesque placerat fermentum ligula. Vivamus sit amet justo quis enim convallis condimentum. Ut non aliquet dui, vel vestibulum libero. In mauris ligula, pharetra eu maximus ut, ultrices ac justo. Donec varius condimentum augue eget tincidunt. Nunc eu egestas est.

    +

    Quisque scelerisque commodo maximus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed egestas et felis consectetur bibendum. Fusce interdum purus nec commodo porta. Praesent gravida efficitur rutrum. Phasellus semper erat urna, vitae hendrerit velit dictum et. Aenean eget dignissim tellus.

    +

    Proin ullamcorper quam quis justo maximus, eget elementum justo porttitor. Duis tellus leo, vestibulum vel felis sit amet, luctus vestibulum arcu. Nullam mauris quam, consequat eget varius nec, pellentesque quis ante. Fusce vitae sollicitudin orci. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Vestibulum ac rutrum ipsum. Nulla in sapien lorem. Etiam sit amet leo eros. Vestibulum lacinia ipsum lobortis lacus congue, a posuere urna sollicitudin. Donec fermentum, ipsum nec tempor dictum, mi sapien sodales mi, eget tempor diam diam ut erat. Integer euismod sit amet tortor ut sollicitudin. Etiam at elit massa. Vivamus faucibus ipsum eget neque semper, at maximus lectus posuere. Nam metus orci, ultricies et lorem at, iaculis placerat diam.

    +

    Vestibulum massa magna, pulvinar id tempus vitae, egestas at eros. Maecenas sollicitudin tincidunt est eget accumsan. Ut ut hendrerit lectus. Pellentesque efficitur lacus in nulla posuere convallis. Ut vulputate erat id odio tincidunt, rhoncus eleifend metus vulputate. Phasellus blandit sem diam, at auctor diam consectetur a. Aliquam sit amet fermentum massa, id ultrices ligula. Aenean tincidunt quam risus, vel aliquet massa tristique at. In lectus nulla, dapibus quis vulputate eu, luctus vel nunc. Duis sollicitudin consequat dui, nec placerat dolor euismod ut. Quisque posuere leo nec accumsan posuere. Vestibulum tristique gravida justo egestas vestibulum. Nunc placerat semper erat vel egestas. Mauris massa sapien, sodales vitae fringilla vel, volutpat dapibus velit.

    +

     

    +
    + + + +" +`; + +exports[`InteropService_Importer_OneNote should expect notes to be rendered the same: A simple filename 1`] = ` +" + + + + A simple filename + + + + +
    A simple filename
    +
    Friday, April 19, 2024
    +
    5:46 PM
    +

     

    +
    + + + +" +`; + +exports[`InteropService_Importer_OneNote should expect notes to be rendered the same: Page with more than one font size 1`] = ` +" + + + + Page with more than one font size + + + + +
    Page with more than one font size
    +
    segunda-feira, 29 de abril de 2024
    +
    10:27
    +

    Suspendisse vitae odio nibh. Etiam fringilla mattis dapibus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce vel ultricies ligula. Sed a nunc ante. Praesent suscipit fermentum magna. Aliquam convallis porttitor lacus ac posuere. Vestibulum maximus leo vel tortor condimentum, et tristique leo maximus. Nulla elementum, augue eu sollicitudin tempus, arcu ex lacinia enim, ut posuere lectus libero non eros. Vestibulum a libero leo. Donec id leo commodo, ornare ante ac, molestie tellus. Aenean a neque quis turpis euismod porta. Quisque vulputate augue vitae orci accumsan, a lobortis leo luctus. Nunc sodales sapien vitae lacus faucibus hendrerit. In ac lacinia diam.

    +

    Nam tempor urna eget posuere mollis. Aliquam erat volutpat. Sed ipsum massa, dictum eget sagittis id, fermentum a justo. Vivamus in iaculis libero. Pellentesque malesuada felis dictum turpis placerat, at ultrices justo viverra. Praesent nisi lectus, tincidunt ut tellus in, convallis euismod urna. Phasellus molestie porttitor odio vitae efficitur. Curabitur vulputate congue tincidunt. Fusce mattis orci at porttitor fermentum. Cras eu placerat odio. Fusce eu tortor sit amet massa pretium efficitur. Nam consequat, mauris at blandit placerat, est sapien feugiat felis, quis imperdiet sapien neque in justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus vestibulum rhoncus dolor, ut ullamcorper purus scelerisque eu. Integer sem felis, pellentesque in rutrum id, porta a ante.Vivamus finibus imperdiet massa, at interdum turpis rhoncus et. Phasellus leo nibh, mattis vel tortor at, gravida finibus felis. Donec bibendum enim euismod, dignissim ipsum eu, laoreet nisl. Ut auctor sollicitudin eros dictum gravida. Vestibulum pellentesque, ex quis vulputate efficitur, dolor metus efficitur nisl, id elementum mi nulla sit amet orci. Nam odio sem, bibendum at hendrerit finibus, vestibulum vitae dolor. In hac habitasse platea dictumst. Curabitur et ligula elit. Donec vulputate, diam non gravida efficitur, mi odio imperdiet ipsum, nec rhoncus mi nibh non magna.

    +

    Suspendisse varius enim vel odio congue sodales. Integer sit amet nisi sagittis, dapibus mi ut, tincidunt magna. Duis posuere est felis, et rhoncus magna volutpat a. Nullam tempor dignissim suscipit. Vestibulum cursus felis vitae libero pulvinar molestie. Donec at metus eget arcu blandit tincidunt. Donec purus felis, malesuada ac egestas eu, interdum sed erat. Praesent nec accumsan orci. Nunc bibendum rutrum erat, vel luctus odio. Pellentesque iaculis gravida arcu, eu consequat turpis congue sit amet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Duis eget urna vel erat aliquet fringilla. Praesent vel luctus ligula, nec viverra nisl. Sed ac sem consectetur, sodales ante sodales, feugiat arcu.

    +

    It was a bright cold day in April, and the clocks were striking thirteen. Winston Smith, his chin nuzzled into his breast in an effort to escape the vile wind, slipped quickly through the glass doors of Victory Mansions, though not quickly enough to prevent a swirl of gritty dust from entering along with him.

    The hallway smelt of boiled cabbage and old rag mats. At one end of it a coloured poster, too large for indoor display, had been tacked to the wall. It depicted simply an enormous face, more than a metre wide: the face of a man of about forty-five, with a heavy black moustache and ruggedly handsome features. Winston made for the stairs. It was no use trying the lift. Even at the best of times it was seldom working, and at present the electric current was cut off during daylight hours. It was part of the economy drive in preparation for Hate Week. The flat was seven flights up, and Winston, who was thirty-nine and had a varicose ulcer above his right ankle, went slowly, resting several times on the way. On each landing, opposite the lift-shaft, the poster with the enormous face gazed from the wall. It was one of those pictures which are so contrived that the eyes follow you about when you move. BIG BROTHER IS WATCHING YOU, the caption beneath it ran.

    Inside the flat a fruity voice was reading out a list of figures which had something to do with the production of pig-iron. The voice came from an oblong metal plaque like a dulled mirror which formed part of the surface of the right-hand wall. Winston turned a switch and the voice sank somewhat, though the words were still distinguishable. The instrument (the telescreen, it was called) could be dimmed, but there was no way of shutting it off completely. He moved over to the window: a smallish, frail figure, the meagreness of his body merely emphasized by the blue overalls which were the uniform of the party. His hair was very fair, his face naturally sanguine, his skin roughened by coarse soap and blunt razor blades and the cold of the winter that had just ended.

    +
    + + + +" +`; + +exports[`InteropService_Importer_OneNote should expect notes to be rendered the same: Quick Notes 1`] = ` +" + + + + Quick Notes + + + + + + + + + + + +" +`; + +exports[`InteropService_Importer_OneNote should expect notes to be rendered the same: text 1`] = ` +" + + + + text + + + + +
    text
    +
    quinta-feira, 25 de abril de 2024
    +
    15:39
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus egestas, enim vel tempor scelerisque, metus magna imperdiet purus, facilisis consequat mi augue eget risus. Vestibulum tincidunt lobortis turpis, id consectetur mauris consectetur et. Aliquam interdum ante ut lectus varius, ut mattis turpis tincidunt. Donec vitae sem sagittis, porta ex a, mattis tortor. Curabitur non velit suscipit, eleifend lectus quis, convallis velit. Nunc a blandit nisl. Cras rhoncus, enim a malesuada commodo, dolor dui pulvinar eros, non lobortis diam velit quis est. Donec pharetra, dolor a faucibus tincidunt, turpis dolor fermentum nibh, facilisis mollis neque urna a neque. Sed sit amet efficitur elit. Duis at elit non quam semper semper non quis mauris. Integer dignissim sodales urna, eu mattis urna viverra sit amet. In porta arcu id mauris hendrerit, in congue erat porta. In hac habitasse platea dictumst. Maecenas finibus sem pharetra blandit suscipit. Proin in nisl ac est pellentesque finibus. Cras ligula tellus, tempor eget leo in, vulputate interdum turpis.

    +

    Nam sit amet massa vehicula, elementum nisl feugiat, fermentum quam. Donec eros urna, ultrices vel fringilla suscipit, pretium non ligula. Sed sit amet pellentesque lorem, quis pharetra augue. Integer vitae sodales ex, luctus imperdiet arcu. Integer luctus urna eu urna ultricies ultricies. Aliquam sit amet maximus orci. Sed molestie vehicula vehicula. Morbi lacinia, dolor eu consectetur commodo, ipsum ante suscipit sem, eget facilisis nibh nisi venenatis magna. Donec ac risus ligula. In sit amet dapibus ante, sit amet pellentesque dolor. Nulla facilisi. Sed a nibh viverra, placerat purus at, rutrum justo. Fusce finibus consequat mattis. Sed felis tellus, consequat id nunc non, cursus tempus ligula. In hac habitasse platea dictumst. Praesent eget consectetur elit, ac mollis est.

    +

    Quisque facilisis justo diam, eget tincidunt augue lobortis non. Quisque rutrum diam sed diam feugiat, quis dictum ex bibendum. Nunc sagittis quam erat, sed pharetra nunc consequat a. Etiam in sollicitudin nunc. Aliquam non dolor laoreet ex egestas efficitur vel ut ligula. Duis mollis ornare laoreet. Nullam vitae velit feugiat leo bibendum faucibus. Morbi nisl nisl, sodales nec sodales vel, consequat in mi. Pellentesque bibendum erat iaculis dui volutpat ornare. Etiam ultricies tincidunt ipsum a congue. In at lacinia massa. Ut auctor id elit et pellentesque.

    +

    Donec ac condimentum dui, tincidunt rhoncus augue. Maecenas aliquam non nisl ac fringilla. Aliquam pulvinar enim sit amet accumsan tristique. Cras sapien ipsum, ultricies eu dui eget, efficitur ornare elit. Curabitur hendrerit mauris dolor, gravida elementum enim convallis quis. Vivamus varius luctus massa, in egestas mi egestas id. Nullam elementum scelerisque nisi sit amet pellentesque. In varius mollis risus, vel laoreet tortor. Pellentesque et blandit velit, nec auctor nunc. Aliquam quis purus vel ligula auctor rhoncus. Duis sed tempus metus. Praesent ac libero sed leo posuere feugiat id vel felis. Aenean commodo dapibus hendrerit. Sed eleifend, tortor sed placerat auctor, dolor dolor efficitur dolor, ut rhoncus eros libero sed ante. Mauris quis blandit sem. Nullam porta urna eros, at viverra sem iaculis in.

    +

     

    +
    + + + +" +`; + +exports[`InteropService_Importer_OneNote should import a simple OneNote notebook: Page title 1`] = ` +" + + + + Page title + + + + +
    Page title
    +
    Friday, May 3, 2024
    +
    6:30 PM
    +

    Page content

    +
    + + + +" +`; diff --git a/packages/lib/shim-init-node.ts b/packages/lib/shim-init-node.ts index eb5a1c26d99..f83b869d8ca 100644 --- a/packages/lib/shim-init-node.ts +++ b/packages/lib/shim-init-node.ts @@ -869,6 +869,12 @@ function shimInit(options: ShimInitOptions = null) { const doc = await loadPdf(pdfPath); return { pageCount: doc.numPages }; }; + + shim.getParentFolderName = (path: string) => { + return require('path').basename( + require('path').dirname(path), + ); + }; } module.exports = { shimInit, setupProxySettings }; diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index 8de50090d4b..04af344346b 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -486,6 +486,9 @@ const shim = { throw new Error('Not implemented'); }, + getParentFolderName: (_path: string): string => { + throw new Error('Not implemented'); + }, }; export default shim; diff --git a/packages/onenote-converter/.gitignore b/packages/onenote-converter/.gitignore new file mode 100644 index 00000000000..17200d6f75a --- /dev/null +++ b/packages/onenote-converter/.gitignore @@ -0,0 +1,5 @@ +/target +/output + +/.idea +*.iml diff --git a/packages/onenote-converter/.vscode/settings.json b/packages/onenote-converter/.vscode/settings.json new file mode 100644 index 00000000000..352a6265a0d --- /dev/null +++ b/packages/onenote-converter/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.linkedProjects": [ + "./Cargo.toml" + ] +} \ No newline at end of file diff --git a/packages/onenote-converter/Cargo.lock b/packages/onenote-converter/Cargo.lock new file mode 100644 index 00000000000..46b36e4f30f --- /dev/null +++ b/packages/onenote-converter/Cargo.lock @@ -0,0 +1,1040 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "approx" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e60b75072ecd4168020818c0107f2857bb6c4e64252d8d3983f6263b40a5c3" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "askama" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134" +dependencies = [ + "askama_derive", + "askama_escape", + "askama_shared", +] + +[[package]] +name = "askama_derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522" +dependencies = [ + "askama_shared", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6083ccb191711e9c2b80b22ee24a8381a18524444914c746d4239e21d1afaf" +dependencies = [ + "askama_escape", + "humansize", + "nom", + "num-traits", + "percent-encoding", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", + "toml", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "backtrace" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "cc" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "color-eyre" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1885697ee8a177096d42f158922251a41973117f6d8a234cee94b9509157b7" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6eee477a4a8a72f4addd4de416eb56d54bc307b284d6601bafdee1f4ea462d1" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-primitive-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c375b9c5eadb68d0a6efee2999fef292f45854c3444c86f09d8ab086ba942b0e" +dependencies = [ + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humansize" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "nom" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a7a9657c84d5814c6196b68bb4429df09c18b1573806259fba397ea4ad0d44" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "onenote-converter" +version = "0.0.1" +dependencies = [ + "askama", + "bytes", + "color-eyre", + "console_error_panic_hook", + "encoding_rs", + "enum-primitive-derive", + "itertools", + "log", + "mime_guess", + "num-traits", + "once_cell", + "palette", + "paste", + "percent-encoding", + "regex", + "sanitize-filename", + "structopt", + "thiserror", + "uuid", + "wasm-bindgen", + "web-sys", + "widestring", +] + +[[package]] +name = "owo-colors" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55" + +[[package]] +name = "palette" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a05c0334468e62a4dfbda34b29110aa7d70d58c7fdb2c9857b5874dd9827cc59" +dependencies = [ + "approx", + "num-traits", + "palette_derive", + "phf", + "phf_codegen", +] + +[[package]] +name = "palette_derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b4b5f600e60dd3a147fb57b4547033d382d1979eb087af310e91cb45a63b1f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "sanitize-filename" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf18934a12018228c5b55a6dae9df5d0641e3566b3630cb46cc55564068e7c2f" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "serde" +version = "1.0.200" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.200" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/packages/onenote-converter/Cargo.toml b/packages/onenote-converter/Cargo.toml new file mode 100644 index 00000000000..4331c3e91ae --- /dev/null +++ b/packages/onenote-converter/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "onenote-converter" +version = "0.0.1" +authors = ["Pedro Luiz "] +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"] \ No newline at end of file diff --git a/packages/onenote-converter/README.md b/packages/onenote-converter/README.md new file mode 100644 index 00000000000..ce0bb2ed6ca --- /dev/null +++ b/packages/onenote-converter/README.md @@ -0,0 +1,59 @@ +# 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`). + + +### Project structure: + +``` +- onenote-converter + - package.json -> file to store scripts on how to build the project + - node_functions.js -> where the custom-made functions used inside rust goes + ... + - pkg -> artifact folder generated by wasm-pack + - package.json -> library that gets used by the Joplin Project + ... + - src + - lib.rs -> starting point +``` + +### How to develop: + +To work with the code you will need: + +- Rust https://www.rust-lang.org/learn/get-started +- wasm-pack https://rustwasm.github.io/wasm-pack/ + +`wasm-pack` is the tool used to compile the Rust code to WebAssembly. + +To build, inside the `onenote-converter/package.json` I added two scripts to build it to release and dev. +For the dev build, when using the code, it might print out a lot of logs, but 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 + + +### Publishing release + +`onenote-converter/package.json` has a `buildProduction` script + + +### 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). But since we opted for not requiring every dev to install +Rust and wasm-pack just to compile this library, we need to acknowledge that depending on how the `.wasm` binary is +added to the project it might be a security concern. + +While the review process is always important, if the binary is added to git by a malicious user he might be able +to include anything he wants inside the .wasm file, so that is why adding a CI step to generate the code is very +important. + +Our idea is to generate the WebAssembly package inside the CI so we can be sure that the code that was generated is +the one that was accepted in the PR. Hopefully, this will be already implemented before this is merged. diff --git a/packages/onenote-converter/askama.toml b/packages/onenote-converter/askama.toml new file mode 100644 index 00000000000..50559d54831 --- /dev/null +++ b/packages/onenote-converter/askama.toml @@ -0,0 +1,2 @@ +[general] +dirs = ["src/templates"] \ No newline at end of file diff --git a/packages/onenote-converter/assets/icons/License b/packages/onenote-converter/assets/icons/License new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/packages/onenote-converter/assets/icons/License @@ -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. diff --git a/packages/onenote-converter/assets/icons/arrow-right-line.svg b/packages/onenote-converter/assets/icons/arrow-right-line.svg new file mode 100755 index 00000000000..f46779f7337 --- /dev/null +++ b/packages/onenote-converter/assets/icons/arrow-right-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/award-line.svg b/packages/onenote-converter/assets/icons/award-line.svg new file mode 100755 index 00000000000..25849f302b4 --- /dev/null +++ b/packages/onenote-converter/assets/icons/award-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/book-open-line.svg b/packages/onenote-converter/assets/icons/book-open-line.svg new file mode 100755 index 00000000000..cbcbbfb9596 --- /dev/null +++ b/packages/onenote-converter/assets/icons/book-open-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/chat-4-line.svg b/packages/onenote-converter/assets/icons/chat-4-line.svg new file mode 100755 index 00000000000..c94a0c60efb --- /dev/null +++ b/packages/onenote-converter/assets/icons/chat-4-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/check-line.svg b/packages/onenote-converter/assets/icons/check-line.svg new file mode 100755 index 00000000000..a28368fca94 --- /dev/null +++ b/packages/onenote-converter/assets/icons/check-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/checkbox-blank-circle-fill.svg b/packages/onenote-converter/assets/icons/checkbox-blank-circle-fill.svg new file mode 100755 index 00000000000..5f7ebd1d99c --- /dev/null +++ b/packages/onenote-converter/assets/icons/checkbox-blank-circle-fill.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/checkbox-blank-circle-line.svg b/packages/onenote-converter/assets/icons/checkbox-blank-circle-line.svg new file mode 100755 index 00000000000..9e627e944be --- /dev/null +++ b/packages/onenote-converter/assets/icons/checkbox-blank-circle-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/checkbox-blank-fill.svg b/packages/onenote-converter/assets/icons/checkbox-blank-fill.svg new file mode 100755 index 00000000000..30364974fff --- /dev/null +++ b/packages/onenote-converter/assets/icons/checkbox-blank-fill.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/checkbox-blank-line.svg b/packages/onenote-converter/assets/icons/checkbox-blank-line.svg new file mode 100755 index 00000000000..c56cacf082b --- /dev/null +++ b/packages/onenote-converter/assets/icons/checkbox-blank-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/checkbox-fill.svg b/packages/onenote-converter/assets/icons/checkbox-fill.svg new file mode 100755 index 00000000000..5c439dbf306 --- /dev/null +++ b/packages/onenote-converter/assets/icons/checkbox-fill.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/contacts-line.svg b/packages/onenote-converter/assets/icons/contacts-line.svg new file mode 100755 index 00000000000..6856442df01 --- /dev/null +++ b/packages/onenote-converter/assets/icons/contacts-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/error-warning-line.svg b/packages/onenote-converter/assets/icons/error-warning-line.svg new file mode 100755 index 00000000000..1df56a6c0bd --- /dev/null +++ b/packages/onenote-converter/assets/icons/error-warning-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/file-list-2-line.svg b/packages/onenote-converter/assets/icons/file-list-2-line.svg new file mode 100755 index 00000000000..0242aa78408 --- /dev/null +++ b/packages/onenote-converter/assets/icons/file-list-2-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/film-line.svg b/packages/onenote-converter/assets/icons/film-line.svg new file mode 100755 index 00000000000..868b3f3fbb6 --- /dev/null +++ b/packages/onenote-converter/assets/icons/film-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/flag-fill.svg b/packages/onenote-converter/assets/icons/flag-fill.svg new file mode 100755 index 00000000000..0a1f84f6853 --- /dev/null +++ b/packages/onenote-converter/assets/icons/flag-fill.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/home-4-line.svg b/packages/onenote-converter/assets/icons/home-4-line.svg new file mode 100755 index 00000000000..a799b6ace49 --- /dev/null +++ b/packages/onenote-converter/assets/icons/home-4-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/lightbulb-line.svg b/packages/onenote-converter/assets/icons/lightbulb-line.svg new file mode 100755 index 00000000000..fe10f634ca7 --- /dev/null +++ b/packages/onenote-converter/assets/icons/lightbulb-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/link.svg b/packages/onenote-converter/assets/icons/link.svg new file mode 100755 index 00000000000..3b7c8e0697e --- /dev/null +++ b/packages/onenote-converter/assets/icons/link.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/lock-line.svg b/packages/onenote-converter/assets/icons/lock-line.svg new file mode 100755 index 00000000000..c9448583773 --- /dev/null +++ b/packages/onenote-converter/assets/icons/lock-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/mark-pen-line.svg b/packages/onenote-converter/assets/icons/mark-pen-line.svg new file mode 100755 index 00000000000..7f742b4a081 --- /dev/null +++ b/packages/onenote-converter/assets/icons/mark-pen-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/music-fill.svg b/packages/onenote-converter/assets/icons/music-fill.svg new file mode 100755 index 00000000000..f0d21411eb9 --- /dev/null +++ b/packages/onenote-converter/assets/icons/music-fill.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/phone-line.svg b/packages/onenote-converter/assets/icons/phone-line.svg new file mode 100755 index 00000000000..2719ef9765d --- /dev/null +++ b/packages/onenote-converter/assets/icons/phone-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/question-mark.svg b/packages/onenote-converter/assets/icons/question-mark.svg new file mode 100755 index 00000000000..487fcd68470 --- /dev/null +++ b/packages/onenote-converter/assets/icons/question-mark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/send-plane-2-line.svg b/packages/onenote-converter/assets/icons/send-plane-2-line.svg new file mode 100755 index 00000000000..fabf7f23ad1 --- /dev/null +++ b/packages/onenote-converter/assets/icons/send-plane-2-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/assets/icons/star-fill.svg b/packages/onenote-converter/assets/icons/star-fill.svg new file mode 100644 index 00000000000..2d59353112d --- /dev/null +++ b/packages/onenote-converter/assets/icons/star-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/onenote-converter/assets/icons/user-line.svg b/packages/onenote-converter/assets/icons/user-line.svg new file mode 100755 index 00000000000..9e64bb5632c --- /dev/null +++ b/packages/onenote-converter/assets/icons/user-line.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/onenote-converter/build.js b/packages/onenote-converter/build.js new file mode 100644 index 00000000000..6bb08040937 --- /dev/null +++ b/packages/onenote-converter/build.js @@ -0,0 +1,8 @@ +const { execSync } = require('child_process'); + +const build = () => { + const profile = process.env.NODE_ENV === 'production' ? '--release' : '--debug'; + return execSync(`wasm-pack build --target nodejs ${profile}`); +}; + +build(); diff --git a/packages/onenote-converter/deny.toml b/packages/onenote-converter/deny.toml new file mode 100644 index 00000000000..40061b126d5 --- /dev/null +++ b/packages/onenote-converter/deny.toml @@ -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" diff --git a/packages/onenote-converter/node_functions.js b/packages/onenote-converter/node_functions.js new file mode 100644 index 00000000000..b0bb47914c7 --- /dev/null +++ b/packages/onenote-converter/node_functions.js @@ -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, +}; diff --git a/packages/onenote-converter/package.json b/packages/onenote-converter/package.json new file mode 100644 index 00000000000..2079463b636 --- /dev/null +++ b/packages/onenote-converter/package.json @@ -0,0 +1,20 @@ +{ + "name": "onenote-builder", + "collaborators": [ + "Pedro Luiz " + ], + "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" + }, + "scripts": { + "buildProduction": "wasm-pack build --release --target nodejs --scope joplin", + "buildDev": "wasm-pack build --dev --target nodejs --scope joplin" + }, + "devDependencies": { + "wasm-pack": "0.12.1" + } +} diff --git a/packages/onenote-converter/pkg/onenote_converter.d.ts b/packages/onenote-converter/pkg/onenote_converter.d.ts new file mode 100644 index 00000000000..62a45046af1 --- /dev/null +++ b/packages/onenote-converter/pkg/onenote_converter.d.ts @@ -0,0 +1,8 @@ +/* tslint:disable */ +/* eslint-disable */ +/** +* @param {string} input +* @param {string} output +* @param {string} base_path +*/ +export function oneNoteConverter(input: string, output: string, base_path: string): void; diff --git a/packages/onenote-converter/pkg/onenote_converter.js b/packages/onenote-converter/pkg/onenote_converter.js new file mode 100644 index 00000000000..344e07ff5e4 --- /dev/null +++ b/packages/onenote-converter/pkg/onenote_converter.js @@ -0,0 +1,372 @@ +let imports = {}; +imports['__wbindgen_placeholder__'] = module.exports; +let wasm; +const { isDirectory, readDir, mkdirSyncRecursive, removePrefix, getOutputPath, getParentDir, normalizeAndWriteFile } = require(String.raw`./snippets/onenote-converter-6981a13478d338f0/node_functions.js`); +const { readFileSync, existsSync } = require(`fs`); +const { basename, extname, dirname, join } = require(`path`); +const { TextEncoder, TextDecoder } = require(`util`); + +const heap = new Array(128).fill(undefined); + +heap.push(undefined, null, true, false); + +function getObject(idx) { return heap[idx]; } + +let WASM_VECTOR_LEN = 0; + +let cachedUint8Memory0 = null; + +function getUint8Memory0() { + if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} + +let cachedTextEncoder = new TextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function isLikeNone(x) { + return x === undefined || x === null; +} + +let cachedInt32Memory0 = null; + +function getInt32Memory0() { + if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) { + cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32Memory0; +} + +let heap_next = heap.length; + +function dropObject(idx) { + if (idx < 132) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +} +/** +* @param {string} input +* @param {string} output +* @param {string} base_path +*/ +module.exports.oneNoteConverter = function(input, output, base_path) { + const ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(output, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passStringToWasm0(base_path, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len2 = WASM_VECTOR_LEN; + wasm.oneNoteConverter(ptr0, len0, ptr1, len1, ptr2, len2); +}; + +module.exports.__wbg_join_13fcc6aa248ce243 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = join(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbindgen_string_get = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; +}; + +module.exports.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); +}; + +module.exports.__wbg_existsSync_fef3d1ee40dd0bc2 = function() { return handleError(function (arg0, arg1) { + const ret = existsSync(getStringFromWasm0(arg0, arg1)); + return ret; +}, arguments) }; + +module.exports.__wbg_isDirectory_8e0fb33be0dac663 = function() { return handleError(function (arg0, arg1) { + const ret = isDirectory(getStringFromWasm0(arg0, arg1)); + return ret; +}, arguments) }; + +module.exports.__wbg_normalizeAndWriteFile_ec3f9624dc064f6f = function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = normalizeAndWriteFile(getStringFromWasm0(arg0, arg1), getArrayU8FromWasm0(arg2, arg3)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_readDir_f1ea9d82729fd652 = function() { return handleError(function (arg0, arg1) { + const ret = readDir(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_readFileSync_dce2cb2a612e5268 = function() { return handleError(function (arg0, arg1) { + const ret = readFileSync(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_mkdirSyncRecursive_b92f184dee879e44 = function() { return handleError(function (arg0, arg1) { + const ret = mkdirSyncRecursive(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_removePrefix_7a999edd559fd0e6 = function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = removePrefix(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_getOutputPath_c8878b16ae903260 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) { + const ret = getOutputPath(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3), getStringFromWasm0(arg4, arg5)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_getParentDir_3ed7ef3bfa04fc4c = function() { return handleError(function (arg0, arg1) { + const ret = getParentDir(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_basename_8509b1ba32f9422a = function() { return handleError(function (arg0, arg1) { + const ret = basename(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_extname_0c314acf648141d7 = function() { return handleError(function (arg0, arg1) { + const ret = extname(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbg_dirname_a04a74019d90c213 = function() { return handleError(function (arg0, arg1) { + const ret = dirname(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); +}, arguments) }; + +module.exports.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); +}; + +module.exports.__wbg_warn_1982e858bdcc0c42 = function(arg0, arg1) { + console.warn(getObject(arg0), getObject(arg1)); +}; + +module.exports.__wbg_buffer_12d079cc21e14bdb = function(arg0) { + const ret = getObject(arg0).buffer; + return addHeapObject(ret); +}; + +module.exports.__wbg_new_63b92bc8671ed464 = function(arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); +}; + +module.exports.__wbg_set_a47bac70306a19a7 = function(arg0, arg1, arg2) { + getObject(arg0).set(getObject(arg1), arg2 >>> 0); +}; + +module.exports.__wbg_length_c20a40f15020d68a = function(arg0) { + const ret = getObject(arg0).length; + return ret; +}; + +module.exports.__wbg_new_abda76e883ba8a5f = function() { + const ret = new Error(); + return addHeapObject(ret); +}; + +module.exports.__wbg_stack_658279fe44541cf6 = function(arg0, arg1) { + const ret = getObject(arg1).stack; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; +}; + +module.exports.__wbg_error_f851667af71bcfc6 = function(arg0, arg1) { + let deferred0_0; + let deferred0_1; + try { + deferred0_0 = arg0; + deferred0_1 = arg1; + console.error(getStringFromWasm0(arg0, arg1)); + } finally { + wasm.__wbindgen_free(deferred0_0, deferred0_1, 1); + } +}; + +module.exports.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len1; + getInt32Memory0()[arg0 / 4 + 0] = ptr1; +}; + +module.exports.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); +}; + +module.exports.__wbindgen_memory = function() { + const ret = wasm.memory; + return addHeapObject(ret); +}; + +const path = require('path').join(__dirname, 'onenote_converter_bg.wasm'); +const bytes = require('fs').readFileSync(path); + +const wasmModule = new WebAssembly.Module(bytes); +const wasmInstance = new WebAssembly.Instance(wasmModule, imports); +wasm = wasmInstance.exports; +module.exports.__wasm = wasm; + diff --git a/packages/onenote-converter/pkg/onenote_converter_bg.wasm b/packages/onenote-converter/pkg/onenote_converter_bg.wasm new file mode 100644 index 00000000000..f08b2b6992b Binary files /dev/null and b/packages/onenote-converter/pkg/onenote_converter_bg.wasm differ diff --git a/packages/onenote-converter/pkg/onenote_converter_bg.wasm.d.ts b/packages/onenote-converter/pkg/onenote_converter_bg.wasm.d.ts new file mode 100644 index 00000000000..e196cea26af --- /dev/null +++ b/packages/onenote-converter/pkg/onenote_converter_bg.wasm.d.ts @@ -0,0 +1,8 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export function oneNoteConverter(a: number, b: number, c: number, d: number, e: number, f: number): void; +export function __wbindgen_malloc(a: number, b: number): number; +export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number; +export function __wbindgen_exn_store(a: number): void; +export function __wbindgen_free(a: number, b: number, c: number): void; diff --git a/packages/onenote-converter/pkg/package.json b/packages/onenote-converter/pkg/package.json new file mode 100644 index 00000000000..9df61acf0ce --- /dev/null +++ b/packages/onenote-converter/pkg/package.json @@ -0,0 +1,23 @@ +{ + "name": "@joplin/onenote-converter", + "collaborators": [ + "Pedro Luiz " + ], + "description": "Convert Microsoft OneNote® notebooks to HTML", + "version": "0.0.1", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/laurent22/joplin" + }, + "files": [ + "onenote_converter_bg.wasm", + "onenote_converter.js", + "onenote_converter.d.ts" + ], + "main": "onenote_converter.js", + "types": "onenote_converter.d.ts", + "keywords": [ + "onenote" + ] +} \ No newline at end of file diff --git a/packages/onenote-converter/pkg/snippets/onenote-converter-6981a13478d338f0/node_functions.js b/packages/onenote-converter/pkg/snippets/onenote-converter-6981a13478d338f0/node_functions.js new file mode 100644 index 00000000000..b0bb47914c7 --- /dev/null +++ b/packages/onenote-converter/pkg/snippets/onenote-converter-6981a13478d338f0/node_functions.js @@ -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, +}; diff --git a/packages/onenote-converter/src/lib.rs b/packages/onenote-converter/src/lib.rs new file mode 100644 index 00000000000..162b2520206 --- /dev/null +++ b/packages/onenote-converter/src/lib.rs @@ -0,0 +1,86 @@ +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] +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(()) +} diff --git a/packages/onenote-converter/src/notebook.rs b/packages/onenote-converter/src/notebook.rs new file mode 100644 index 00000000000..b868c5adc46 --- /dev/null +++ b/packages/onenote-converter/src/notebook.rs @@ -0,0 +1,120 @@ +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, log_warn}; +use crate::utils::{join_path, make_dir, remove_prefix, write_file}; +use crate::{section, templates}; +use color_eyre::eyre::{eyre, Result}; +use palette::rgb::Rgb; +use palette::{Alpha, ConvertFrom, Hsl, Saturate, Shade, Srgb}; +use std::path::Path; + +pub(crate) type RgbColor = Alpha, 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, + )) + } + } + } + + let toc_html = templates::notebook::render(name, &toc)?; + let toc_path = unsafe { join_path(output_dir, format!("{}.html", name).as_str()) } + .unwrap() + .as_string() + .unwrap(); + // let _ = unsafe { write_file(toc_path.as_str(), toc_html.as_bytes()) }; + + Ok(()) + } + + fn render_section( + &mut self, + section: &Section, + notebook_dir: String, + base_dir: String, + ) -> Result { + 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(), + } +} diff --git a/packages/onenote-converter/src/page/content.rs b/packages/onenote-converter/src/page/content.rs new file mode 100644 index 00000000000..f9e41dea6ae --- /dev/null +++ b/packages/onenote-converter/src/page/content.rs @@ -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 { + 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()) + } + } + } +} diff --git a/packages/onenote-converter/src/page/embedded_file.rs b/packages/onenote-converter/src/page/embedded_file.rs new file mode 100644 index 00000000000..391e94e653b --- /dev/null +++ b/packages/onenote-converter/src/page/embedded_file.rs @@ -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 { + 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!("", filename), + FileType::Video => content = format!("", filename), + FileType::Unknown => { + content = format!( + "

    {}

    ", + 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 { + 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; + } + } +} diff --git a/packages/onenote-converter/src/page/image.rs b/packages/onenote-converter/src/page/image.rs new file mode 100644 index 00000000000..03cf7b4d914 --- /dev/null +++ b/packages/onenote-converter/src/page/image.rs @@ -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 { + 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!("", attrs.to_string())); + } + + Ok(self.render_with_note_tags(image.note_tags(), content)) + } + + fn determine_image_filename(&mut self, image: &Image) -> Result { + 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!() + } +} diff --git a/packages/onenote-converter/src/page/ink.rs b/packages/onenote-converter/src/page/ink.rs new file mode 100644 index 00000000000..0ccd9ab21ad --- /dev/null +++ b/packages/onenote-converter/src/page/ink.rs @@ -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_styles.to_string(), + attrs.to_string(), + path + ) + } else { + format!("{}", 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!("", 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 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()) +} diff --git a/packages/onenote-converter/src/page/list.rs b/packages/onenote-converter/src/page/list.rs new file mode 100644 index 00000000000..1cc09355930 --- /dev/null +++ b/packages/onenote-converter/src/page/list.rs @@ -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, + indents: &[f32], + ) -> Result { + 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) -> 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, + ) { + 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() + } +} diff --git a/packages/onenote-converter/src/page/mod.rs b/packages/onenote-converter/src/page/mod.rs new file mode 100644 index 00000000000..e66571e9107 --- /dev/null +++ b/packages/onenote-converter/src/page/mod.rs @@ -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, + global_classes: HashSet, +} + +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 { + 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!("
    ", styles.to_string()); + + for outline in title.contents() { + title_field.push_str(&self.render_outline(outline)?) + } + + title_field.push_str("
    "); + + content.push_str(&title_field); + } + + let page_content = page + .contents() + .iter() + .map(|content| self.render_page_content(content)) + .collect::>()?; + + 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 { + 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()), + } + } +} diff --git a/packages/onenote-converter/src/page/note_tag.rs b/packages/onenote-converter/src/page/note_tag.rs new file mode 100644 index 00000000000..da88b47b42c --- /dev/null +++ b/packages/onenote-converter/src/page/note_tag.rs @@ -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!("
    {}", styles, markup)); + contents.push_str(&content); + contents.push_str("
    "); + + 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!( + "{}", + 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, "1") + } + + fn icon_checkbox_with_2( + &self, + status: ActionItemStatus, + style: StyleSet, + color: &'static str, + ) -> (Cow<'static, str>, StyleSet) { + self.icon_checkbox_with(status, style, color, "2") + } + + fn icon_checkbox_with_3( + &self, + status: ActionItemStatus, + style: StyleSet, + color: &'static str, + ) -> (Cow<'static, str>, StyleSet) { + self.icon_checkbox_with(status, style, color, "3") + } + + fn icon_checkbox_with_exclamation( + &self, + status: ActionItemStatus, + style: StyleSet, + color: &'static str, + ) -> (Cow<'static, str>, StyleSet) { + self.icon_checkbox_with(status, style, color, "!") + } + + 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!( + "{}", + 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 + } +} diff --git a/packages/onenote-converter/src/page/outline.rs b/packages/onenote-converter/src/page/outline.rs new file mode 100644 index 00000000000..65a15cab627 --- /dev/null +++ b/packages/onenote-converter/src/page/outline.rs @@ -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 { + 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!("
    ", attrs)); + contents.push_str(&self.render_outline_items( + outline.items(), + 0, + outline.child_level(), + outline.indents(), + )?); + contents.push_str("
    "); + + Ok(contents) + } + + pub(crate) fn render_outline_items( + &mut self, + items: &[OutlineItem], + parent_level: u8, + current_level: u8, + indents: &[f32], + ) -> Result { + 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 { + 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!("
  • ", attrs)); + } else { + contents.push_str(&format!("
    ", attrs)); + } + + self.in_list = is_list; + + contents.extend( + element + .contents() + .iter() + .map(|content| self.render_content(content)) + .collect::, _>>()? + .into_iter(), + ); + + self.in_list = false; + + if !is_list { + contents.push_str("
    "); + } + + 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("
  • "); + } + + contents.push('\n'); + + Ok(contents) + } +} + +fn flatten_outline_items<'a>( + items: &'a [OutlineItem], + parent_level: u8, + current_level: u8, +) -> Box + '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(), + ), + })) +} diff --git a/packages/onenote-converter/src/page/rich_text.rs b/packages/onenote-converter/src/page/rich_text.rs new file mode 100644 index 00000000000..d8e84c8c902 --- /dev/null +++ b/packages/onenote-converter/src/page/rich_text.rs @@ -0,0 +1,296 @@ +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 { + 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!("{}", 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!("{}", style, content)), + _ => Ok(content), + } + } + + fn parse_content(&mut self, data: &RichText) -> Result { + 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!("", + px(space.width()), px(space.height())) + } + EmbeddedObject::InkLineBreak => { + "
    ".to_string() + } + }) + .collect_vec() + .join("")); + } + + let indices = data.text_run_indices(); + let styles = data.text_run_formatting(); + + let mut text = data.text().to_string(); + + if text.is_empty() { + text = " ".to_string(); + } + + 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 = 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!("{}", style, text)) + } else { + Ok(text) + } + } + }) + .collect::>()?; + + Ok(fix_newlines(&content)) + } + + fn render_hyperlink( + &self, + text: String, + style: &ParagraphStyling, + in_hyperlink: bool, + ) -> Result { + 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!("", url, style)) + } else if in_hyperlink { + Ok(text + "") + } else { + Ok(format!( + "{}", + 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 = + Lazy::new(|| Regex::new(r"
    (\s+)").expect("failed to compile regex")); + + let text = text + .replace("\u{000b}", "
    ") + .replace("\n", "
    ") + .replace("\r", "
    "); + + REGEX_LEADING_SPACES + .replace_all(&text, |captures: &Captures| { + "
    ".to_string() + &" ".repeat(captures[1].len()) + }) + .to_string() +} diff --git a/packages/onenote-converter/src/page/table.rs b/packages/onenote-converter/src/page/table.rs new file mode 100644 index 00000000000..0d039a875ef --- /dev/null +++ b/packages/onenote-converter/src/page/table.rs @@ -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 { + 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!("", 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(""); + + 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(""); + } + + content.push_str("
    "); + + Ok(self.render_with_note_tags(table.note_tags(), content)) + } + + fn render_table_cell( + &mut self, + contents: &mut String, + cell: &TableCell, + width: Option, + ) -> 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!("", 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(""); + + 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 { + if data.is_empty() { + return vec![false; count as usize]; + } + + (0..count) + .map(|i| data[i as usize / 8] & (1 << (i % 8)) == 1) + .collect() +} diff --git a/packages/onenote-converter/src/parser/errors.rs b/packages/onenote-converter/src/parser/errors.rs new file mode 100644 index 00000000000..ba20a4dba61 --- /dev/null +++ b/packages/onenote-converter/src/parser/errors.rs @@ -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 = std::result::Result; + +/// 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 for Error { + fn from(kind: ErrorKind) -> Self { + Error { kind } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Self { + ErrorKind::from(err).into() + } +} + +impl From for Error { + fn from(err: std::string::FromUtf16Error) -> Self { + ErrorKind::from(err).into() + } +} + +impl From for Error { + fn from(err: widestring::error::MissingNulTerminator) -> Self { + ErrorKind::from(err).into() + } +} + +impl From 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, + }, +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data/binary_item.rs b/packages/onenote-converter/src/parser/fsshttpb/data/binary_item.rs new file mode 100644 index 00000000000..cb080e59f55 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data/binary_item.rs @@ -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); + +impl BinaryItem { + pub(crate) fn parse(reader: Reader) -> Result { + let size = CompactU64::parse(reader)?.value(); + let data = reader.read(size as usize)?.to_vec(); + + Ok(BinaryItem(data)) + } + + pub(crate) fn value(self) -> Vec { + self.0 + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data/cell_id.rs b/packages/onenote-converter/src/parser/fsshttpb/data/cell_id.rs new file mode 100644 index 00000000000..cbac1702fe0 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data/cell_id.rs @@ -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 { + let first = ExGuid::parse(reader)?; + let second = ExGuid::parse(reader)?; + + Ok(CellId(first, second)) + } + + pub(crate) fn parse_array(reader: Reader) -> Result> { + let mut values = vec![]; + + let count = CompactU64::parse(reader)?.value(); + for _ in 0..count { + values.push(CellId::parse(reader)?); + } + + Ok(values) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data/compact_u64.rs b/packages/onenote-converter/src/parser/fsshttpb/data/compact_u64.rs new file mode 100644 index 00000000000..cea2eb5ae66 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data/compact_u64.rs @@ -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 { + 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 + ); + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data/exguid.rs b/packages/onenote-converter/src/parser/fsshttpb/data/exguid.rs new file mode 100644 index 00000000000..fa25191e9d1 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data/exguid.rs @@ -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 { + 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 { + 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> { + 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) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data/mod.rs b/packages/onenote-converter/src/parser/fsshttpb/data/mod.rs new file mode 100644 index 00000000000..715c96cc3c5 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data/mod.rs @@ -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; diff --git a/packages/onenote-converter/src/parser/fsshttpb/data/object_types.rs b/packages/onenote-converter/src/parser/fsshttpb/data/object_types.rs new file mode 100644 index 00000000000..c9d10bfa237 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data/object_types.rs @@ -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) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data/serial_number.rs b/packages/onenote-converter/src/parser/fsshttpb/data/serial_number.rs new file mode 100644 index 00000000000..0a733469669 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data/serial_number.rs @@ -0,0 +1,34 @@ +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)] +pub struct SerialNumber { + pub guid: Guid, + pub serial: u64, +} + +impl SerialNumber { + pub(crate) fn parse(reader: Reader) -> Result { + 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 }) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data/stream_object.rs b/packages/onenote-converter/src/parser/fsshttpb/data/stream_object.rs new file mode 100644 index 00000000000..3f8c6c52af8 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data/stream_object.rs @@ -0,0 +1,233 @@ +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)] +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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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, + ) -> 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, + ) -> 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), + } + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data_element/cell_manifest.rs b/packages/onenote-converter/src/parser/fsshttpb/data_element/cell_manifest.rs new file mode 100644 index 00000000000..2ed32eb9147 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data_element/cell_manifest.rs @@ -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 { + ObjectHeader::try_parse_16(reader, ObjectType::CellManifest)?; + + let id = ExGuid::parse(reader)?; + + ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?; + + Ok(id) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data_element/data_element_fragment.rs b/packages/onenote-converter/src/parser/fsshttpb/data_element/data_element_fragment.rs new file mode 100644 index 00000000000..372c9102c46 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data_element/data_element_fragment.rs @@ -0,0 +1,54 @@ +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)] +pub(crate) struct DataElementFragment { + pub(crate) id: ExGuid, + pub(crate) size: u64, + pub(crate) chunk_reference: DataElementFragmentChunkReference, + pub(crate) data: Vec, +} + +#[derive(Debug)] +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 { + 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) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data_element/mod.rs b/packages/onenote-converter/src/parser/fsshttpb/data_element/mod.rs new file mode 100644 index 00000000000..2f1d62812d4 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data_element/mod.rs @@ -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, + pub(crate) storage_manifests: HashMap, + pub(crate) cell_manifests: HashMap, + pub(crate) revision_manifests: HashMap, + pub(crate) object_groups: HashMap, + pub(crate) data_element_fragments: HashMap, + pub(crate) object_data_blobs: HashMap, +} + +impl DataElementPackage { + pub(crate) fn parse(reader: Reader) -> Result { + 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> { + 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::>() + } + + /// 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 { + 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(()) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data_element/object_data_blob.rs b/packages/onenote-converter/src/parser/fsshttpb/data_element/object_data_blob.rs new file mode 100644 index 00000000000..da3088d518c --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data_element/object_data_blob.rs @@ -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); + +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 { + ObjectHeader::try_parse(reader, ObjectType::ObjectDataBlob)?; + + let data = BinaryItem::parse(reader)?; + + ObjectHeader::try_parse_end_8(reader, ObjectType::DataElement)?; + + Ok(ObjectDataBlob(data.value())) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data_element/object_group.rs b/packages/onenote-converter/src/parser/fsshttpb/data_element/object_group.rs new file mode 100644 index 00000000000..ff69b6d3a58 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data_element/object_group.rs @@ -0,0 +1,333 @@ +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)] +pub(crate) struct ObjectGroup { + pub(crate) declarations: Vec, + pub(crate) metadata: Vec, + pub(crate) objects: Vec, +} + +/// 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)] +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)] +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, + cells: Vec, + data: Vec, + }, + /// 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, + cells: Vec, + 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, + cells: Vec, + 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 { + 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> { + 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> { + 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> { + 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) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data_element/revision_manifest.rs b/packages/onenote-converter/src/parser/fsshttpb/data_element/revision_manifest.rs new file mode 100644 index 00000000000..c7de3f2cc35 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data_element/revision_manifest.rs @@ -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, + pub(crate) group_references: Vec, +} + +/// 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 { + 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 { + 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) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data_element/storage_index.rs b/packages/onenote-converter/src/parser/fsshttpb/data_element/storage_index.rs new file mode 100644 index 00000000000..583e71bf580 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data_element/storage_index.rs @@ -0,0 +1,138 @@ +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)] +pub(crate) struct StorageIndex { + pub(crate) manifest_mappings: Vec, + pub(crate) cell_mappings: HashMap, + pub(crate) revision_mappings: HashMap, +} + +impl StorageIndex { + pub(crate) fn find_cell_mapping_id(&self, cell_id: CellId) -> Option { + self.cell_mappings.get(&cell_id).map(|mapping| mapping.id) + } + + pub(crate) fn find_revision_mapping_id(&self, id: ExGuid) -> Option { + self.revision_mappings + .get(&id) + .map(|mapping| mapping.revision_mapping) + } +} + +/// A storage indexes manifest mapping. +#[derive(Debug)] +pub(crate) struct StorageIndexManifestMapping { + pub(crate) mapping_id: ExGuid, + pub(crate) serial: SerialNumber, +} + +/// A storage indexes cell mapping. +#[derive(Debug)] +pub(crate) struct StorageIndexCellMapping { + pub(crate) cell_id: CellId, + pub(crate) id: ExGuid, + pub(crate) serial: SerialNumber, +} + +/// A storage indexes revision mapping. +#[derive(Debug)] +pub(crate) struct StorageIndexRevisionMapping { + pub(crate) revision_mapping: ExGuid, + pub(crate) serial: SerialNumber, +} + +impl DataElement { + pub(crate) fn parse_storage_index(reader: Reader) -> Result { + 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 { + 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)) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/data_element/storage_manifest.rs b/packages/onenote-converter/src/parser/fsshttpb/data_element/storage_manifest.rs new file mode 100644 index 00000000000..dd27b6f03dc --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/data_element/storage_manifest.rs @@ -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, +} + +impl DataElement { + pub(crate) fn parse_storage_manifest(reader: Reader) -> Result { + 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 }) + } +} diff --git a/packages/onenote-converter/src/parser/fsshttpb/mod.rs b/packages/onenote-converter/src/parser/fsshttpb/mod.rs new file mode 100644 index 00000000000..fc55774d7ff --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/mod.rs @@ -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; diff --git a/packages/onenote-converter/src/parser/fsshttpb/packaging.rs b/packages/onenote-converter/src/parser/fsshttpb/packaging.rs new file mode 100644 index 00000000000..a2dec8fe9b4 --- /dev/null +++ b/packages/onenote-converter/src/parser/fsshttpb/packaging.rs @@ -0,0 +1,61 @@ +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)] +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 { + 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, + }) + } +} diff --git a/packages/onenote-converter/src/parser/macros.rs b/packages/onenote-converter/src/parser/macros.rs new file mode 100644 index 00000000000..7d505abe9db --- /dev/null +++ b/packages/onenote-converter/src/parser/macros.rs @@ -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 + ) + ); + } +} diff --git a/packages/onenote-converter/src/parser/mod.rs b/packages/onenote-converter/src/parser/mod.rs new file mode 100644 index 00000000000..367cce87d3d --- /dev/null +++ b/packages/onenote-converter/src/parser/mod.rs @@ -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; + } +} diff --git a/packages/onenote-converter/src/parser/one/mod.rs b/packages/onenote-converter/src/parser/one/mod.rs new file mode 100644 index 00000000000..3789d9c8bf7 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/mod.rs @@ -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; diff --git a/packages/onenote-converter/src/parser/one/property/author.rs b/packages/onenote-converter/src/parser/one/property/author.rs new file mode 100644 index 00000000000..edf48b5251f --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/author.rs @@ -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> { + Ok(simple::parse_string(PropertyType::Author, object)?.map(Author)) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/charset.rs b/packages/onenote-converter/src/parser/one/property/charset.rs new file mode 100644 index 00000000000..1700d776724 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/charset.rs @@ -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> { + 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)) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/color.rs b/packages/onenote-converter/src/parser/one/property/color.rs new file mode 100644 index 00000000000..3bc3385709c --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/color.rs @@ -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> { + 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], + })) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/color_ref.rs b/packages/onenote-converter/src/parser/one/property/color_ref.rs new file mode 100644 index 00000000000..2f74bec1a1e --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/color_ref.rs @@ -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> { + 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)) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/file_type.rs b/packages/onenote-converter/src/parser/one/property/file_type.rs new file mode 100644 index 00000000000..7cbd841cc69 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/file_type.rs @@ -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 { + 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) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/ink_dimensions.rs b/packages/onenote-converter/src/parser/one/property/ink_dimensions.rs new file mode 100644 index 00000000000..549bd0fcdd7 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/ink_dimensions.rs @@ -0,0 +1,41 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; +use crate::parser::reader::Reader; +use crate::parser::shared::guid::Guid; + +/// The dimensions (X or Y) for an ink stoke with lower and upper limits. +#[allow(dead_code)] +pub(crate) struct InkDimension { + pub(crate) id: Guid, + pub(crate) limit_lower: i32, + pub(crate) limit_upper: i32, +} + +impl InkDimension { + pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result> { + let data = match object.props().get(prop_type) { + Some(value) => value.to_vec().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("ink dimensions is not a vec".into()) + })?, + None => return Ok(Vec::new()), + }; + + data.chunks_exact(32) + .map(InkDimension::parse_entry) + .collect::>>() + } + + fn parse_entry(data: &[u8]) -> Result { + let mut reader = Reader::new(data); + let id = Guid::parse(&mut reader)?; + let limit_lower = reader.get_u32()? as i32; + let limit_upper = reader.get_u32()? as i32; + + Ok(InkDimension { + id, + limit_lower, + limit_upper, + }) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/layout_alignment.rs b/packages/onenote-converter/src/parser/one/property/layout_alignment.rs new file mode 100644 index 00000000000..26213d6ee4c --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/layout_alignment.rs @@ -0,0 +1,148 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; + +/// A layout alignment specification. +/// +/// See [\[MS-ONE\] 2.3.2] +/// +/// [\[MS-ONE\] 2.3.2]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/bd99face-5839-4276-863d-a4f2cbb81246 +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct LayoutAlignment { + alignment_horizontal: HorizontalAlignment, + alignment_margin_horizontal: HorizontalAlignmentMargin, + alignment_vertical: VerticalAlignment, + alignment_margin_vertical: VerticalAlignmentMargin, +} + +impl LayoutAlignment { + pub fn alignment_horizontal(&self) -> HorizontalAlignment { + self.alignment_horizontal + } + + pub fn alignment_margin_horizontal(&self) -> HorizontalAlignmentMargin { + self.alignment_margin_horizontal + } + + pub fn alignment_vertical(&self) -> VerticalAlignment { + self.alignment_vertical + } + + pub fn alignment_margin_vertical(&self) -> VerticalAlignmentMargin { + self.alignment_margin_vertical + } +} + +impl LayoutAlignment { + pub(crate) fn parse( + prop_type: PropertyType, + object: &Object, + ) -> Result> { + let value = match object.props().get(prop_type) { + Some(value) => value.to_u32().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("layout alignment is not a u32".into()) + })?, + None => return Ok(None), + }; + + if (value >> 31) & 0x1 != 0 { + return Ok(None); + } + + let alignment_horizontal = HorizontalAlignment::parse(value & 0x7)?; + let alignment_margin_horizontal = HorizontalAlignmentMargin::parse((value >> 3) & 0x1)?; + let alignment_vertical = VerticalAlignment::parse((value >> 16) & 0x1)?; + let alignment_margin_vertical = VerticalAlignmentMargin::parse((value >> 19) & 0x1)?; + + Ok(Some(LayoutAlignment { + alignment_horizontal, + alignment_margin_horizontal, + alignment_vertical, + alignment_margin_vertical, + })) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum HorizontalAlignment { + Unknown, + Left, + Center, + Right, + BiDiNormal, + BiDiReverse, +} + +impl HorizontalAlignment { + pub(crate) fn parse(value: u32) -> Result { + match value { + 0 => Ok(HorizontalAlignment::Unknown), + 1 => Ok(HorizontalAlignment::Left), + 2 => Ok(HorizontalAlignment::Center), + 3 => Ok(HorizontalAlignment::Right), + 4 => Ok(HorizontalAlignment::BiDiNormal), + 5 => Ok(HorizontalAlignment::BiDiReverse), + _ => Err(ErrorKind::MalformedOneNoteFileData( + format!("invalid horizontal alignment: {}", value).into(), + ) + .into()), + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum HorizontalAlignmentMargin { + Right, + Left, +} + +impl HorizontalAlignmentMargin { + pub(crate) fn parse(value: u32) -> Result { + match value { + 0 => Ok(HorizontalAlignmentMargin::Right), + 1 => Ok(HorizontalAlignmentMargin::Left), + _ => Err(ErrorKind::MalformedOneNoteFileData( + format!("invalid horizontal alignment margin: {}", value).into(), + ) + .into()), + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum VerticalAlignment { + Bottom, + Top, +} + +impl VerticalAlignment { + pub(crate) fn parse(value: u32) -> Result { + match value { + 0 => Ok(VerticalAlignment::Bottom), + 1 => Ok(VerticalAlignment::Top), + _ => Err(ErrorKind::MalformedOneNoteFileData( + format!("invalid vertical alignment: {}", value).into(), + ) + .into()), + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum VerticalAlignmentMargin { + Bottom, + Top, +} + +impl VerticalAlignmentMargin { + pub(crate) fn parse(value: u32) -> Result { + match value { + 0 => Ok(VerticalAlignmentMargin::Bottom), + 1 => Ok(VerticalAlignmentMargin::Top), + _ => Err(ErrorKind::MalformedOneNoteFileData( + format!("invalid vertical alignment margin: {}", value).into(), + ) + .into()), + } + } +} diff --git a/packages/onenote-converter/src/parser/one/property/mod.rs b/packages/onenote-converter/src/parser/one/property/mod.rs new file mode 100644 index 00000000000..04c3a681076 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/mod.rs @@ -0,0 +1,208 @@ +//! The properties of OneNote objects that we can parse. +//! +//! See [\[MS-ONE\] 2.1.12] for the list of properties the OneNote file format specifies. +//! +//! [\[MS-ONE\] 2.1.12]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/e9bf7da8-7aab-4668-be5e-e0c421175e3c + +pub(crate) mod author; +pub(crate) mod charset; +pub(crate) mod color; +pub(crate) mod color_ref; +pub(crate) mod file_type; +pub(crate) mod ink_dimensions; +pub(crate) mod layout_alignment; +pub(crate) mod note_tag; +pub(crate) mod note_tag_property_status; +pub(crate) mod note_tag_shape; +pub(crate) mod object_reference; +pub(crate) mod object_space_reference; +pub(crate) mod outline_indent_distance; +pub(crate) mod page_size; +pub(crate) mod paragraph_alignment; +mod references; +pub(crate) mod simple; +pub(crate) mod time; + +#[derive(Debug, Copy, Clone, PartialEq)] +#[allow(dead_code)] +pub(crate) enum PropertyType { + ActionItemSchemaVersion = 0x0C003473, + ActionItemStatus = 0x10003470, + ActionItemType = 0x10003463, + Author = 0x1C001D75, + AuthorMostRecent = 0x20001D79, + AuthorOriginal = 0x20001D78, + BodyTextAlignment = 0x0C001C13, + Bold = 0x08001C04, + CachedTitleString = 0x1C001CF3, + CachedTitleStringFromPage = 0x1C001D3C, + CannotBeSelected = 0x08001CB2, + Charset = 0x0C001D01, + ChildGraphSpaceElementNodes = 0x2C001D63, + ColumnCount = 0x14001D58, + ConflictingUserName = 0x1C001D9E, + ContentChildNodes = 0x24001C1F, + CreationTimeStamp = 0x14001D09, + Deletable = 0x08001D0C, + DescendantsCannotBeMoved = 0x08001CF9, + DisplayedPageNumber = 0x14003480, + EditRootRtl = 0x08001C92, + ElementChildNodes = 0x24001C20, + EmbeddedFileContainer = 0x20001D9B, + EmbeddedFileName = 0x1C001D9C, + EnableHistory = 0x08001E1E, + EnforceOutlineStructure = 0x08001C91, + FileAncestorIdentityGuid = 0x1C001D95, + FileIdentityGuid = 0x1C001D94, + FileLastCodeVersionThatWroteToIt = 0x14001D99, + FileNameCrc = 0x14001D93, + Font = 0x1C001C0A, + FontColor = 0x14001C0C, + FontSize = 0x10001C0B, + HasVersionPages = 0x08003462, + Hidden = 0x08001E16, + Highlight = 0x14001C0D, + Hyperlink = 0x08001E14, + HyperlinkProtected = 0x08001E19, + IRecordMedia = 0x14001D24, + ImageAltText = 0x1C001E58, + ImageFilename = 0x1C001DD7, + ImageUploadState = 0x140034CB, + IsBackground = 0x08001D13, + IsBoilerText = 0x08001C88, + IsConflictObjectForRender = 0x08001D96, + IsConflictObjectForSelection = 0x08001DDB, + IsConflictPage = 0x08001D7C, + IsDeletedGraphSpaceContent = 0x00001DE9, + IsLayoutSizeSetByUser = 0x08001CBD, + IsReadOnly = 0x08001CDE, + IsTitleDate = 0x08001CB5, + IsTitleText = 0x08001CB4, + IsTitleTime = 0x08001C87, + Italic = 0x08001C05, + LanguageId = 0x14001C3B, + LastModifiedTime = 0x14001D7A, + LastModifiedTimeStamp = 0x18001D77, + LayoutAlignmentInParent = 0x14001C3E, + LayoutAlignmentSelf = 0x14001C84, + LayoutCollisionPriority = 0x14001CF1, + LayoutMaxHeight = 0x14001C1C, + LayoutMaxWidth = 0x14001C1B, + LayoutMinimumOutlineWidth = 0x14001CEC, + LayoutOutlineReservedWidth = 0x14001CDB, + LayoutResolveChildCollisions = 0x08001CDC, + LayoutTightAlignment = 0x08001CFF, + LayoutTightLayout = 0x08001C00, + ListFont = 0x1C001C52, + ListMsaaIndex = 0x10001D0E, + ListNodes = 0x24001C26, + ListRestart = 0x14001CB7, + ListSpacingMu = 0x14001CCB, + MathFormatting = 0x08003401, + MetaDataObjectsAboveGraphSpace = 0x24003442, + NextStyle = 0x1C00348A, + NoteTagCompleted = 0x1400346F, + NoteTagCreated = 0x1400346E, + NoteTagDefinitionOid = 0x20003488, + NoteTagHighlightColor = 0x14003465, + NoteTagLabel = 0x1C003468, + NoteTagPropertyStatus = 0x14003467, + NoteTagShape = 0x10003464, + NoteTagStates = 0x04003489, + NoteTagTextColor = 0x14003466, + NotebookManagementEntityGuid = 0x1C001C30, + NumberListFormat = 0x1C001C1A, + OffsetFromParentHoriz = 0x14001C14, + OffsetFromParentVert = 0x14001C15, + OutlineElementChildLevel = 0x0C001C03, + OutlineElementRtl = 0x08001C34, + PageHeight = 0x14001C02, + PageLevel = 0x14001DFF, + PageMarginBottom = 0x14001C4D, + PageMarginLeft = 0x14001C4E, + PageMarginOriginX = 0x14001D0F, + PageMarginOriginY = 0x14001D10, + PageMarginRight = 0x14001C4F, + PageMarginTop = 0x14001C4C, + PageSize = 0x14001C8B, + PageWidth = 0x14001C01, + ParagraphAlignment = 0x0C003477, + ParagraphLineSpacingExact = 0x14003430, + ParagraphSpaceAfter = 0x1400342F, + ParagraphSpaceBefore = 0x1400342E, + ParagraphStyle = 0x2000342C, + ParagraphStyleId = 0x1C00345A, + PictureContainer = 0x20001C3F, + PictureHeight = 0x140034CE, + PictureWidth = 0x140034CD, + PortraitPage = 0x08001C8E, + ReadingOrderRtl = 0x08003476, + RgOutlineIndentDistance = 0x1C001C12, + RichEditTextLangId = 0x10001CFE, + RichEditTextUnicode = 0x1C001C22, + RowCount = 0x14001D57, + SchemaRevisionInOrderToRead = 0x14001D82, + SchemaRevisionInOrderToWrite = 0x1400348B, + SectionDisplayName = 0x1C00349B, + SourceFilepath = 0x1C001D9D, + Strikethrough = 0x08001C07, + StructureElementChildNodes = 0x24001D5F, + Subscript = 0x08001C09, + Superscript = 0x08001C08, + TableBordersVisible = 0x08001D5E, + TableColumnWidths = 0x1C001D66, + TableColumnsLocked = 0x1C001D7D, + TaskTagDueDat = 0x1400346B, + TextExtendedAscii = 0x1C003498, + TextRunData = 0x40003499, + TextRunDataObject = 0x24003458, + TextRunFormatting = 0x24001E13, + TextRunIndex = 0x1C001E12, + TextRunIsEmbeddedObject = 0x08001E22, + TopologyCreationTimeStamp = 0x18001C65, + Underline = 0x08001C06, + UnderlineType = 0x0C001E15, + VersionHistoryGraphSpaceContextNodes = 0x3400347B, + WebPictureContainer14 = 0x200034C8, + WzHyperlinkUrl = 0x1C001E20, + + // Undocumented: + TocChildren = 0x24001CF6, + FolderChildFilename = 0x1C001D6B, + NotebookElementOrderingId = 0x14001CB9, + PictureFileExtension = 0x24003424, + NoteTags = 0x40003489, + NoteTag = 0x44000811, + SectionColor = 0x14001CBE, + CellBackgroundColor = 0x14001E26, + InkBias = 0x0C00341C, + InkData = 0x20003415, + InkDimensions = 0x1C00340A, + InkPath = 0x1C00340B, + InkStrokeProperties = 0x20003409, + InkStrokes = 0x24003416, + // TODO: Fix values not being accepted as valid enum values + // InkAntialised = 0x8000340E, + // InkFitToCurve = 0x80003410, + InkIgnorePressure = 0x08003411, + InkPenTip = 0x0C003412, + InkRasterOperation = 0x0C003413, + InkTransparency = 0x0C003414, + InkHeight = 0x1400340C, + InkWidth = 0x1400340D, + InkColor = 0x1400340F, + InkScalingX = 0x14001C46, + InkScalingY = 0x14001C47, + InkBoundingBox = 0x1C003418, + EmbeddedObjectType = 0x14003457, + EmbeddedInkStartX = 0x1400349E, + EmbeddedInkStartY = 0x1400349F, + EmbeddedInkWidth = 0x140034A0, + EmbeddedInkHeight = 0x140034A1, + EmbeddedInkOffsetHoriz = 0x140034A2, + EmbeddedInkOffsetVert = 0x140034A3, + EmbeddedInkSpaceWidth = 0x14001C27, + EmbeddedInkSpaceHeight = 0x14001C28, + ImageEmbedType = 0x140035F2, + ImageEmbeddedUrl = 0x1C0035F3, +} diff --git a/packages/onenote-converter/src/parser/one/property/note_tag.rs b/packages/onenote-converter/src/parser/one/property/note_tag.rs new file mode 100644 index 00000000000..5e610e689dd --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/note_tag.rs @@ -0,0 +1,95 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; + +/// The action status of a note tag. +/// +/// See [\[MS-ONE\] 2.3.91]. +/// +/// [\[MS-ONE\] 2.3.91]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/6b516f12-8f47-40b3-9dd4-44c00aac206b +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct ActionItemStatus { + completed: bool, + disabled: bool, + task_tag: bool, +} + +impl ActionItemStatus { + /// Whether the checkable note tag is completed. + pub fn completed(&self) -> bool { + self.completed + } + + /// Whether the note tag is disabled. + pub fn disabled(&self) -> bool { + self.disabled + } + + /// Whether the note tag is a task tag. + pub fn task_tag(&self) -> bool { + self.task_tag + } +} + +impl ActionItemStatus { + pub(crate) fn parse(object: &Object) -> Result> { + let value = match object.props().get(PropertyType::ActionItemStatus) { + Some(value) => value.to_u16().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("action item status is not a u16".into()) + })?, + None => return Ok(None), + }; + + let completed = value & 0x1 != 0; + let disabled = (value >> 1) & 0x1 != 0; + let task_tag = (value >> 2) & 0x1 != 0; + + Ok(Some(ActionItemStatus { + completed, + disabled, + task_tag, + })) + } +} + +/// The identifier and item type of a note tag. +/// +/// See [\[MS-ONE\] 2.3.85]. +/// +/// [\[MS-ONE\] 2.3.85]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/b39a1d88-b8e1-48c6-bbfe-99ac3effe91b +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[allow(missing_docs)] +pub enum ActionItemType { + Numeric(u16), + DueToday, + DueTomorrow, + DueThisWeek, + DueNextWeek, + NoDueDate, + CustomDueDate, + Unknown, +} + +impl ActionItemType { + pub(crate) fn parse(object: &Object) -> Result> { + let value = match object.props().get(PropertyType::ActionItemType) { + Some(value) => value.to_u16().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("action item type is not a u16".into()) + })?, + None => return Ok(None), + }; + + let item_type = match value { + 0..=99 => ActionItemType::Numeric(value), + 100 => ActionItemType::DueToday, + 101 => ActionItemType::DueTomorrow, + 102 => ActionItemType::DueThisWeek, + 103 => ActionItemType::DueNextWeek, + 104 => ActionItemType::NoDueDate, + 105 => ActionItemType::CustomDueDate, + _ => ActionItemType::Unknown, + }; + + Ok(Some(item_type)) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/note_tag_property_status.rs b/packages/onenote-converter/src/parser/one/property/note_tag_property_status.rs new file mode 100644 index 00000000000..4244036d7ba --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/note_tag_property_status.rs @@ -0,0 +1,100 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; + +/// The status of a note tag. +/// +/// See [\[MS-ONE\] 2.3.87]. +/// +/// [\[MS-ONE\] 2.3.87]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/24274836-ec41-4fee-913f-225d65ac457c +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct NoteTagPropertyStatus { + has_label: bool, + has_font_color: bool, + has_highlight_color: bool, + has_icon: bool, + due_today: bool, + due_tomorrow: bool, + due_this_week: bool, + due_next_week: bool, + due_later: bool, + due_custom: bool, +} + +impl NoteTagPropertyStatus { + /// Whether the note tag has a labe. + pub fn has_label(&self) -> bool { + self.has_label + } + + /// Whether the note tag has a font color. + pub fn has_font_color(&self) -> bool { + self.has_font_color + } + + /// Whether the note tag has a text highlight color. + pub fn has_highlight_color(&self) -> bool { + self.has_highlight_color + } + + /// Whether the note tag has an icon. + pub fn has_icon(&self) -> bool { + self.has_icon + } + + /// Whether the note tag has is due today. + pub fn due_today(&self) -> bool { + self.due_today + } + + /// Whether the note tag has is due tomorrow. + pub fn due_tomorrow(&self) -> bool { + self.due_tomorrow + } + + /// Whether the note tag has is due this week. + pub fn due_this_week(&self) -> bool { + self.due_this_week + } + + /// Whether the note tag has is due next week. + pub fn due_next_week(&self) -> bool { + self.due_next_week + } + + /// Whether the note tag has is due later. + pub fn due_later(&self) -> bool { + self.due_later + } + + /// Whether the note tag has is due at a custom date. + pub fn due_custom(&self) -> bool { + self.due_custom + } +} + +impl NoteTagPropertyStatus { + pub(crate) fn parse(object: &Object) -> Result> { + let value = match object.props().get(PropertyType::NoteTagPropertyStatus) { + Some(value) => value.to_u32().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("note tag property status is not a u32".into()) + })?, + None => return Ok(None), + }; + + let status = NoteTagPropertyStatus { + has_label: value & 0x1 != 0, + has_font_color: (value >> 1) & 0x1 != 0, + has_highlight_color: (value >> 2) & 0x1 != 0, + has_icon: (value >> 3) & 0x1 != 0, + due_today: (value >> 6) & 0x1 != 0, + due_tomorrow: (value >> 7) & 0x1 != 0, + due_this_week: (value >> 8) & 0x1 != 0, + due_next_week: (value >> 9) & 0x1 != 0, + due_later: (value >> 10) & 0x1 != 0, + due_custom: (value >> 11) & 0x1 != 0, + }; + + Ok(Some(status)) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/note_tag_shape.rs b/packages/onenote-converter/src/parser/one/property/note_tag_shape.rs new file mode 100644 index 00000000000..00893a4a908 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/note_tag_shape.rs @@ -0,0 +1,344 @@ +/// The shape of a note tag icon. +/// +/// See [\[MS-ONE\] 2.3.86]. +/// +/// [\[MS-ONE\] 2.3.86]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/aa5c7aba-d5e4-4bf8-b265-9100eeb9a7a7 +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[allow(missing_docs)] +pub enum NoteTagShape { + NoIcon, + GreenCheckBox, + YellowCheckBox, + BlueCheckBox, + GreenStarCheckBox, + YellowStarCheckBox, + BlueStarCheckBox, + GreenExclamationCheckBox, + YellowExclamationCheckBox, + BlueExclamationCheckBox, + GreenRightArrowCheckBox, + YellowRightArrowCheckBox, + BlueRightArrowCheckBox, + YellowStar, + BlueFollowUpFlag, + QuestionMark, + BlueRightArrow, + HighPriority, + ContactInformation, + Meeting, + TimeSensitive, + LightBulb, + Pushpin, + Home, + CommentBubble, + SmilingFace, + AwardRibbon, + YellowKey, + BlueCheckBox1, + BlueCircle1, + BlueCheckBox2, + BlueCircle2, + BlueCheckBox3, + BlueCircle3, + BlueEightPointStar, + BlueCheckMark, + BlueCircle, + BlueDownArrow, + BlueLeftArrow, + BlueSolidTarget, + BlueStar, + BlueSun, + BlueTarget, + BlueTriangle, + BlueUmbrella, + BlueUpArrow, + BlueXWithDots, + BlueX, + GreenCheckBox1, + GreenCircle1, + GreenCheckBox2, + GreenCircle2, + GreenCheckBox3, + GreenCircle3, + GreenEightPointStar, + GreenCheckMark, + GreenCircle, + GreenDownArrow, + GreenLeftArrow, + GreenRightArrow, + GreenSolidArrow, + GreenStar, + GreenSun, + GreenTarget, + GreenTriangle, + GreenUmbrella, + GreenUpArrow, + GreenXWithDots, + GreenX, + YellowCheckBox1, + YellowCircle1, + YellowCheckBox2, + YellowCircle2, + YellowCheckBox3, + YellowCircle3, + YellowEightPointStar, + YellowCheckMark, + YellowCircle, + YellowDownArrow, + YellowLeftArrow, + YellowRightArrow, + YellowSolidTarget, + YellowSun, + YellowTarget, + YellowTriangle, + YellowUmbrella, + YellowUpArrow, + YellowXWithDots, + YellowX, + FollowUpTodayFlag, + FollowUpTomorrowFlag, + FollowUpThisWeekFlag, + FollowUpNextWeekFlag, + NoFollowUpDateFlag, + BluePersonCheckBox, + YellowPersonCheckBox, + GreenPersonCheckBox, + BlueFlagCheckBox, + RedFlagCheckBox, + GreenFlagCheckBox, + RedSquare, + YellowSquare, + BlueSquare, + GreenSquare, + OrangeSquare, + PinkSquare, + EMailMessage, + ClosedEnvelope, + OpenEnvelope, + MobilePhone, + TelephoneWithClock, + QuestionBalloon, + PaperClip, + FrowningFace, + InstantMessagingContactPerson, + PersonWithExclamationMark, + TwoPeople, + ReminderBell, + Contact, + RoseOnAStem, + CalendarDateWithClock, + MusicalNote, + MovieClip, + QuotationMark, + Globe, + HyperlinkGlobe, + Laptop, + Plane, + Car, + Binoculars, + PresentationSlide, + Padlock, + OpenBook, + NotebookWithClock, + BlankPaperWithLines, + Research, + Pen, + DollarSign, + CoinsWithAWindowBackdrop, + ScheduledTask, + LightningBolt, + Cloud, + Heart, + Sunflower, +} + +impl NoteTagShape { + pub(crate) fn parse(value: u16) -> NoteTagShape { + match value { + 0 => NoteTagShape::NoIcon, + 1 => NoteTagShape::GreenCheckBox, + 2 => NoteTagShape::YellowCheckBox, + 3 => NoteTagShape::BlueCheckBox, + 4 => NoteTagShape::GreenStarCheckBox, + 5 => NoteTagShape::YellowStarCheckBox, + 6 => NoteTagShape::BlueStarCheckBox, + 7 => NoteTagShape::GreenExclamationCheckBox, + 8 => NoteTagShape::YellowExclamationCheckBox, + 9 => NoteTagShape::BlueExclamationCheckBox, + 10 => NoteTagShape::GreenRightArrowCheckBox, + 11 => NoteTagShape::YellowRightArrowCheckBox, + 12 => NoteTagShape::BlueRightArrowCheckBox, + 13 => NoteTagShape::YellowStar, + 14 => NoteTagShape::BlueFollowUpFlag, + 15 => NoteTagShape::QuestionMark, + 16 => NoteTagShape::BlueRightArrow, + 17 => NoteTagShape::HighPriority, + 18 => NoteTagShape::ContactInformation, + 19 => NoteTagShape::Meeting, + 20 => NoteTagShape::TimeSensitive, + 21 => NoteTagShape::LightBulb, + 22 => NoteTagShape::Pushpin, + 23 => NoteTagShape::Home, + 24 => NoteTagShape::CommentBubble, + 25 => NoteTagShape::SmilingFace, + 26 => NoteTagShape::AwardRibbon, + 27 => NoteTagShape::YellowKey, + 28 => NoteTagShape::BlueCheckBox1, + 29 => NoteTagShape::BlueCircle1, + 30 => NoteTagShape::BlueCheckBox2, + 31 => NoteTagShape::BlueCircle2, + 32 => NoteTagShape::BlueCheckBox3, + 33 => NoteTagShape::BlueCircle3, + 34 => NoteTagShape::BlueEightPointStar, + 35 => NoteTagShape::BlueCheckMark, + 36 => NoteTagShape::BlueCircle, + 37 => NoteTagShape::BlueDownArrow, + 38 => NoteTagShape::BlueLeftArrow, + 39 => NoteTagShape::BlueSolidTarget, + 40 => NoteTagShape::BlueStar, + 41 => NoteTagShape::BlueSun, + 42 => NoteTagShape::BlueTarget, + 43 => NoteTagShape::BlueTriangle, + 44 => NoteTagShape::BlueUmbrella, + 45 => NoteTagShape::BlueUpArrow, + 46 => NoteTagShape::BlueXWithDots, + 47 => NoteTagShape::BlueX, + 48 => NoteTagShape::GreenCheckBox1, + 49 => NoteTagShape::GreenCircle1, + 50 => NoteTagShape::GreenCheckBox2, + 51 => NoteTagShape::GreenCircle2, + 52 => NoteTagShape::GreenCheckBox3, + 53 => NoteTagShape::GreenCircle3, + 54 => NoteTagShape::GreenEightPointStar, + 55 => NoteTagShape::GreenCheckMark, + 56 => NoteTagShape::GreenCircle, + 57 => NoteTagShape::GreenDownArrow, + 58 => NoteTagShape::GreenLeftArrow, + 59 => NoteTagShape::GreenRightArrow, + 60 => NoteTagShape::GreenSolidArrow, + 61 => NoteTagShape::GreenStar, + 62 => NoteTagShape::GreenSun, + 63 => NoteTagShape::GreenTarget, + 64 => NoteTagShape::GreenTriangle, + 65 => NoteTagShape::GreenUmbrella, + 66 => NoteTagShape::GreenUpArrow, + 67 => NoteTagShape::GreenXWithDots, + 68 => NoteTagShape::GreenX, + 69 => NoteTagShape::YellowCheckBox1, + 70 => NoteTagShape::YellowCircle1, + 71 => NoteTagShape::YellowCheckBox2, + 72 => NoteTagShape::YellowCircle2, + 73 => NoteTagShape::YellowCheckBox3, + 74 => NoteTagShape::YellowCircle3, + 75 => NoteTagShape::YellowEightPointStar, + 76 => NoteTagShape::YellowCheckMark, + 77 => NoteTagShape::YellowCircle, + 78 => NoteTagShape::YellowDownArrow, + 79 => NoteTagShape::YellowLeftArrow, + 80 => NoteTagShape::YellowRightArrow, + 81 => NoteTagShape::YellowSolidTarget, + 82 => NoteTagShape::YellowSun, + 83 => NoteTagShape::YellowTarget, + 84 => NoteTagShape::YellowTriangle, + 85 => NoteTagShape::YellowUmbrella, + 86 => NoteTagShape::YellowUpArrow, + 87 => NoteTagShape::YellowXWithDots, + 88 => NoteTagShape::YellowX, + 89 => NoteTagShape::FollowUpTodayFlag, + 90 => NoteTagShape::FollowUpTomorrowFlag, + 91 => NoteTagShape::FollowUpThisWeekFlag, + 92 => NoteTagShape::FollowUpNextWeekFlag, + 93 => NoteTagShape::NoFollowUpDateFlag, + 94 => NoteTagShape::BluePersonCheckBox, + 95 => NoteTagShape::YellowPersonCheckBox, + 96 => NoteTagShape::GreenPersonCheckBox, + 97 => NoteTagShape::BlueFlagCheckBox, + 98 => NoteTagShape::RedFlagCheckBox, + 99 => NoteTagShape::GreenFlagCheckBox, + 100 => NoteTagShape::RedSquare, + 101 => NoteTagShape::YellowSquare, + 102 => NoteTagShape::BlueSquare, + 103 => NoteTagShape::GreenSquare, + 104 => NoteTagShape::OrangeSquare, + 105 => NoteTagShape::PinkSquare, + 106 => NoteTagShape::EMailMessage, + 107 => NoteTagShape::ClosedEnvelope, + 108 => NoteTagShape::OpenEnvelope, + 109 => NoteTagShape::MobilePhone, + 110 => NoteTagShape::TelephoneWithClock, + 111 => NoteTagShape::QuestionBalloon, + 112 => NoteTagShape::PaperClip, + 113 => NoteTagShape::FrowningFace, + 114 => NoteTagShape::InstantMessagingContactPerson, + 115 => NoteTagShape::PersonWithExclamationMark, + 116 => NoteTagShape::TwoPeople, + 117 => NoteTagShape::ReminderBell, + 118 => NoteTagShape::Contact, + 119 => NoteTagShape::RoseOnAStem, + 120 => NoteTagShape::CalendarDateWithClock, + 121 => NoteTagShape::MusicalNote, + 122 => NoteTagShape::MovieClip, + 123 => NoteTagShape::QuotationMark, + 124 => NoteTagShape::Globe, + 125 => NoteTagShape::HyperlinkGlobe, + 126 => NoteTagShape::Laptop, + 127 => NoteTagShape::Plane, + 128 => NoteTagShape::Car, + 129 => NoteTagShape::Binoculars, + 130 => NoteTagShape::PresentationSlide, + 131 => NoteTagShape::Padlock, + 132 => NoteTagShape::OpenBook, + 133 => NoteTagShape::NotebookWithClock, + 134 => NoteTagShape::BlankPaperWithLines, + 135 => NoteTagShape::Research, + 136 => NoteTagShape::Pen, + 137 => NoteTagShape::DollarSign, + 138 => NoteTagShape::CoinsWithAWindowBackdrop, + 139 => NoteTagShape::ScheduledTask, + 140 => NoteTagShape::LightningBolt, + 141 => NoteTagShape::Cloud, + 142 => NoteTagShape::Heart, + 143 => NoteTagShape::Sunflower, + _ => panic!("invalid note tag shape: {}", value), + } + } + + /// Whether the note tag icon is checkable. + pub fn is_checkable(&self) -> bool { + matches!( + self, + NoteTagShape::GreenCheckBox + | NoteTagShape::YellowCheckBox + | NoteTagShape::BlueCheckBox + | NoteTagShape::GreenStarCheckBox + | NoteTagShape::YellowStarCheckBox + | NoteTagShape::BlueStarCheckBox + | NoteTagShape::GreenExclamationCheckBox + | NoteTagShape::YellowExclamationCheckBox + | NoteTagShape::BlueExclamationCheckBox + | NoteTagShape::GreenRightArrowCheckBox + | NoteTagShape::YellowRightArrowCheckBox + | NoteTagShape::BlueRightArrowCheckBox + | NoteTagShape::BlueCheckBox1 + | NoteTagShape::BlueCheckBox2 + | NoteTagShape::BlueCheckBox3 + | NoteTagShape::GreenCheckBox1 + | NoteTagShape::GreenCheckBox2 + | NoteTagShape::GreenCheckBox3 + | NoteTagShape::YellowCheckBox1 + | NoteTagShape::YellowCheckBox2 + | NoteTagShape::YellowCheckBox3 + | NoteTagShape::FollowUpTodayFlag + | NoteTagShape::FollowUpTomorrowFlag + | NoteTagShape::FollowUpThisWeekFlag + | NoteTagShape::FollowUpNextWeekFlag + | NoteTagShape::NoFollowUpDateFlag + | NoteTagShape::BluePersonCheckBox + | NoteTagShape::YellowPersonCheckBox + | NoteTagShape::GreenPersonCheckBox + | NoteTagShape::BlueFlagCheckBox + | NoteTagShape::RedFlagCheckBox + | NoteTagShape::GreenFlagCheckBox + ) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/object_reference.rs b/packages/onenote-converter/src/parser/one/property/object_reference.rs new file mode 100644 index 00000000000..aa513ce1eee --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/object_reference.rs @@ -0,0 +1,93 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::fsshttpb::data::exguid::ExGuid; +use crate::parser::one::property::references::References; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; +use crate::parser::onestore::types::compact_id::CompactId; +use crate::parser::onestore::types::property::PropertyValue; + +/// A generic object reference. +/// +/// This allows for all sorts of object references (e.g. pages referencing their content). +/// It implements parsing these references from the OneStore mapping table. +pub(crate) struct ObjectReference; + +impl ObjectReference { + pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result> { + // Validate the value of the property + match object.props().get(prop_type) { + Some(property) => property.to_object_id().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("object reference is not a object id".into()) + })?, + None => return Ok(None), + }; + + // Find the correct object reference + let index = Self::get_offset(prop_type, object)?; + + let id = object + .props() + .object_ids() + .iter() + .nth(index) + .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("object id index corrupt".into()))?; + + Ok(Self::resolve_id(index, id, object)) + } + + pub(crate) fn parse_vec( + prop_type: PropertyType, + object: &Object, + ) -> Result>> { + // Determine the number of object references + let count = match object.props().get(prop_type) { + Some(prop) => prop.to_object_ids().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData( + "object reference array is not a object id array".into(), + ) + })?, + None => return Ok(None), + }; + + // Determine offset for the property for which we want to look up the object reference + let offset = Self::get_offset(prop_type, object)?; + + let references = object.props().object_ids(); + + // Look up the object references by offset/count and resolve them + let object_ids = references + .iter() + .skip(offset) + .take(count as usize) + .enumerate() + .flat_map(|(index, id)| Self::resolve_id(index + offset, id, object)) + .collect(); + + Ok(Some(object_ids)) + } + + pub(crate) fn get_offset(prop_type: PropertyType, object: &Object) -> Result { + let predecessors = References::get_predecessors(prop_type, object)?; + let offset = Self::count_references(predecessors); + + Ok(offset) + } + + pub(crate) fn count_references<'a>(props: impl Iterator) -> usize { + props + .map(|v| match v { + PropertyValue::ObjectId => 1, + PropertyValue::ObjectIds(c) => *c as usize, + PropertyValue::PropertyValues(_, sets) => sets + .iter() + .map(|set| Self::count_references(set.values())) + .sum(), + _ => 0, + }) + .sum() + } + + fn resolve_id(index: usize, id: &CompactId, object: &Object) -> Option { + object.mapping().get_object(index, *id) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/object_space_reference.rs b/packages/onenote-converter/src/parser/one/property/object_space_reference.rs new file mode 100644 index 00000000000..8a8dbfbea45 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/object_space_reference.rs @@ -0,0 +1,76 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::fsshttpb::data::cell_id::CellId; +use crate::parser::one::property::references::References; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; +use crate::parser::onestore::types::compact_id::CompactId; +use crate::parser::onestore::types::property::PropertyValue; + +/// A generic object space reference. +/// +/// This allows for all sorts of object space references (e.g. sections referencing their pages). +/// It implements parsing these references from the OneStore mapping table. +pub(crate) struct ObjectSpaceReference; + +impl ObjectSpaceReference { + pub(crate) fn parse_vec( + prop_type: PropertyType, + object: &Object, + ) -> Result>> { + // Determine the number of object space references + let count = match object.props().get(prop_type) { + Some(prop) => prop.to_object_space_ids().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData( + "object space reference array is not a object id array".into(), + ) + })?, + None => return Ok(None), + }; + + // Determine offset for the property for which we want to look up the object space + // reference + let offset = Self::get_offset(prop_type, object)?; + + let references = object.props().object_space_ids(); + + // Look up the object space references by offset/count and resolve them + let object_space_ids = references + .iter() + .skip(offset) + .take(count as usize) + .enumerate() + .flat_map(|(index, id)| Self::resolve_id(index, id, object)) + .collect(); + + Ok(Some(object_space_ids)) + } + + pub(crate) fn get_offset(prop_type: PropertyType, object: &Object) -> Result { + let predecessors = References::get_predecessors(prop_type, object)?; + let offset = Self::count_references(predecessors); + + Ok(offset) + } + + pub(crate) fn count_references<'a>(props: impl Iterator) -> usize { + props + .map(|v| match v { + PropertyValue::ObjectSpaceId => 1, + PropertyValue::ObjectSpaceIds(c) => *c as usize, + PropertyValue::PropertyValues(_, sets) => sets + .iter() + .map(|set| Self::count_references(set.values())) + .sum(), + _ => 0, + }) + .sum() + } + + fn resolve_id(index: usize, id: &CompactId, object: &Object) -> Result { + object + .mapping() + .get_object_space(index, *id) + .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("id not defined in mapping".into())) + .map_err(|e| e.into()) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/outline_indent_distance.rs b/packages/onenote-converter/src/parser/one/property/outline_indent_distance.rs new file mode 100644 index 00000000000..349c73e4fbd --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/outline_indent_distance.rs @@ -0,0 +1,36 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; +use crate::parser::reader::Reader; + +#[derive(Debug, Clone)] +pub struct OutlineIndentDistance(Vec); + +impl OutlineIndentDistance { + pub fn value(&self) -> &[f32] { + &self.0 + } + + pub(crate) fn into_value(self) -> Vec { + self.0 + } + + pub(crate) fn parse(object: &Object) -> Result> { + let value = match object.props().get(PropertyType::RgOutlineIndentDistance) { + Some(value) => value.to_vec().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("outline indent distance is not a vec".into()) + })?, + None => return Ok(None), + }; + + let mut reader = Reader::new(value); + let count = reader.get_u8()?; + reader.advance(3)?; + + let distances = (0..count) + .map(|_| reader.get_f32()) + .collect::>>()?; + + Ok(Some(OutlineIndentDistance(distances))) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/page_size.rs b/packages/onenote-converter/src/parser/one/property/page_size.rs new file mode 100644 index 00000000000..ca1fe9a3f5d --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/page_size.rs @@ -0,0 +1,75 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; + +/// A page size declaration. +/// +/// See [\[MS-ONE\] 2.3.36]. +/// +/// [\[MS-ONE\] 2.3.36]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/8866c05a-602d-4868-95de-2d8b1a0b9d2e +#[derive(Debug)] +pub(crate) enum PageSize { + Auto, + Us, + AnsiLetter, + AnsiTabloid, + UsLegal, + IsoA3, + IsoA4, + IsoA5, + IsoA6, + JisB4, + JisB5, + JisB6, + JapanesePostcard, + IndexCard, + Billfold, + Custom, +} + +impl PageSize { + pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result> { + let value = match object.props().get(prop_type) { + Some(value) => value.try_to_u8().ok_or_else(|| { + ErrorKind::MalformedOneNoteIncorrectType(format!( + "page size is not a u8 but {:?}", + value + )) + })?, + None => return Ok(None), + }; + + let page_size = match value { + 0 => PageSize::Auto, + 1 => PageSize::Us, + 2 => PageSize::AnsiLetter, + 3 => PageSize::AnsiTabloid, + 4 => PageSize::UsLegal, + 5 => PageSize::IsoA3, + 6 => PageSize::IsoA4, + 7 => PageSize::IsoA5, + 8 => PageSize::IsoA6, + 9 => PageSize::JisB4, + 10 => PageSize::JisB5, + 11 => PageSize::JisB6, + 12 => PageSize::JapanesePostcard, + 13 => PageSize::IndexCard, + 14 => PageSize::Billfold, + 15 => PageSize::Custom, + _ => { + return Err(ErrorKind::MalformedOneNoteFileData( + format!("invalid page size: {}", value).into(), + ) + .into()) + } + }; + + Ok(Some(page_size)) + } +} + +impl Default for PageSize { + fn default() -> Self { + PageSize::Auto + } +} diff --git a/packages/onenote-converter/src/parser/one/property/paragraph_alignment.rs b/packages/onenote-converter/src/parser/one/property/paragraph_alignment.rs new file mode 100644 index 00000000000..0f9380bdf76 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/paragraph_alignment.rs @@ -0,0 +1,44 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; + +/// A paragraph's alignment. +/// +/// See [\[MS-ONE\] 2.3.94]. +/// +/// [\[MS-ONE\] 2.3.94]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/36edb135-5e8e-400f-9394-82853d662d90 +#[allow(missing_docs)] +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ParagraphAlignment { + Unknown, + Left, + Center, + Right, +} + +impl ParagraphAlignment { + pub(crate) fn parse(object: &Object) -> Result> { + let value = match object.props().get(PropertyType::ParagraphAlignment) { + Some(value) => value.try_to_u8().ok_or_else(|| { + ErrorKind::MalformedOneNoteIncorrectType(format!( + "page size is not a u8 but {:?}", + value + )) + })?, + None => return Ok(None), + }; + + Ok(Some(match value { + 0 => ParagraphAlignment::Left, + 1 => ParagraphAlignment::Center, + 2 => ParagraphAlignment::Right, + _ => ParagraphAlignment::Unknown, + })) + } +} + +impl Default for ParagraphAlignment { + fn default() -> Self { + ParagraphAlignment::Left + } +} diff --git a/packages/onenote-converter/src/parser/one/property/references.rs b/packages/onenote-converter/src/parser/one/property/references.rs new file mode 100644 index 00000000000..007dad686aa --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/references.rs @@ -0,0 +1,32 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; +use crate::parser::onestore::types::property::{PropertyId, PropertyValue}; + +pub(crate) struct References; + +impl References { + pub(crate) fn get_predecessors<'a>( + prop_type: PropertyType, + object: &'a Object<'a>, + ) -> Result> { + let prop_index = object + .props() + .properties() + .index(PropertyId::new(prop_type as u32)) + .ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData( + format!("no object offset for property {:?}", prop_type).into(), + ) + })?; + + let predecessors = object + .props() + .properties() + .values_with_index() + .filter(move |(idx, _)| *idx < prop_index) + .map(|(_, value)| value); + + Ok(predecessors) + } +} diff --git a/packages/onenote-converter/src/parser/one/property/simple.rs b/packages/onenote-converter/src/parser/one/property/simple.rs new file mode 100644 index 00000000000..5ad30410877 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/simple.rs @@ -0,0 +1,159 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; +use crate::parser::reader::Reader; +use crate::parser::shared::guid::Guid; +use crate::parser::utils::Utf16ToString; +use encoding_rs::mem::decode_latin1; + +pub(crate) fn parse_bool(prop_type: PropertyType, object: &Object) -> Result> { + let value = match object.props().get(prop_type) { + Some(value) => value.to_bool().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("bool value is not a bool".into()) + })?, + None => return Ok(None), + }; + + Ok(Some(value)) +} + +pub(crate) fn parse_u8(prop_type: PropertyType, object: &Object) -> Result> { + let value = match object.props().get(prop_type) { + Some(value) => value + .to_u8() + .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("u8 value is not a u8".into()))?, + None => return Ok(None), + }; + + Ok(Some(value)) +} + +pub(crate) fn parse_u16(prop_type: PropertyType, object: &Object) -> Result> { + let value = match object.props().get(prop_type) { + Some(value) => value + .to_u16() + .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("u16 value is not a u16".into()))?, + None => return Ok(None), + }; + + Ok(Some(value)) +} + +pub(crate) fn parse_u32(prop_type: PropertyType, object: &Object) -> Result> { + let value = match object.props().get(prop_type) { + Some(value) => value + .to_u32() + .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("u32 value is not a u32".into()))?, + None => return Ok(None), + }; + + Ok(Some(value)) +} + +// Not used at the moment +// +// pub(crate) fn parse_u64(prop_type: PropertyType, object: &Object) -> Result> { +// object +// .props() +// .get(prop_type) +// .map(|value| { +// value +// .to_u64() +// .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("u64 value is not a u64".into())) +// }) +// .transpose() +// .map_err(|e| e.into()) +// } + +pub(crate) fn parse_f32(prop_type: PropertyType, object: &Object) -> Result> { + let value = match object.props().get(prop_type) { + Some(value) => value.to_u32().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("float value is not a u32".into()) + })?, + None => return Ok(None), + }; + + Ok(Some(f32::from_le_bytes(value.to_le_bytes()))) +} + +pub(crate) fn parse_vec(prop_type: PropertyType, object: &Object) -> Result>> { + let data = match object.props().get(prop_type) { + Some(value) => value + .to_vec() + .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("vec value is not a vec".into()))?, + None => return Ok(None), + }; + + Ok(Some(data.to_vec())) +} + +pub(crate) fn parse_vec_u16(prop_type: PropertyType, object: &Object) -> Result>> { + let data = match object.props().get(prop_type) { + Some(value) => value.to_vec().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("vec u16 value is not a vec".into()) + })?, + None => return Ok(None), + }; + + let vec = data + .chunks_exact(2) + .map(|v| u16::from_le_bytes([v[0], v[1]])) + .collect(); + + Ok(Some(vec)) +} + +pub(crate) fn parse_vec_u32(prop_type: PropertyType, object: &Object) -> Result>> { + let data = match object.props().get(prop_type) { + Some(value) => value + .to_vec() + .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("vec value is not a vec".into()))?, + None => return Ok(None), + }; + + let vec = data + .chunks_exact(4) + .map(|v| u32::from_le_bytes([v[0], v[1], v[2], v[3]])) + .collect(); + + Ok(Some(vec)) +} + +pub(crate) fn parse_ascii(prop_type: PropertyType, object: &Object) -> Result> { + let data = match object.props().get(prop_type) { + Some(value) => value.to_vec().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("ascii value is not a vec".into()) + })?, + None => return Ok(None), + }; + + let text = decode_latin1(data).to_string(); + + Ok(Some(text)) +} + +pub(crate) fn parse_string(prop_type: PropertyType, object: &Object) -> Result> { + let data = match object.props().get(prop_type) { + Some(value) => value + .to_vec() + .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("vec value is not a vec".into()))?, + None => return Ok(None), + }; + + let text = data + .utf16_to_string() + .map_err(|_| ErrorKind::MalformedOneNoteFileData("invalid string".into()))?; + + Ok(Some(text)) +} + +pub(crate) fn parse_guid(prop_type: PropertyType, object: &Object) -> Result> { + let data = match object.props().get(prop_type) { + Some(value) => value + .to_vec() + .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("guid value is not a vec".into()))?, + None => return Ok(None), + }; + + Ok(Some(Guid::parse(&mut Reader::new(data))?)) +} diff --git a/packages/onenote-converter/src/parser/one/property/time.rs b/packages/onenote-converter/src/parser/one/property/time.rs new file mode 100644 index 00000000000..a374c999d84 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property/time.rs @@ -0,0 +1,53 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::PropertyType; +use crate::parser::onestore::object::Object; + +/// A 32 bit date/time timestamp. +/// +/// See [\[MS-ONE\] 2.3.1] +/// +/// [\[MS-ONE\] 2.3.1]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/82336580-f956-40ea-94ab-d9ab15048395 +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub struct Time(u32); + +impl Time { + pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result> { + let time = object + .props() + .get(prop_type) + .map(|value| { + value.to_u32().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("time value is not a u32".into()) + }) + }) + .transpose()? + .map(Time); + + Ok(time) + } +} + +/// A 64 bit date/time timestamp. +/// +/// See [\[MS-DTYP\] 2.3.3] +/// +/// [\[MS-DTYP\] 2.3.3]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/2c57429b-fdd4-488f-b5fc-9e4cf020fcdf +#[derive(Debug)] +pub(crate) struct Timestamp(u64); + +impl Timestamp { + pub(crate) fn parse(prop_type: PropertyType, object: &Object) -> Result> { + let timestamp = object + .props() + .get(prop_type) + .map(|value| { + value.to_u64().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("timestamp value is not a u64".into()) + }) + }) + .transpose()? + .map(Timestamp); + + Ok(timestamp) + } +} diff --git a/packages/onenote-converter/src/parser/one/property_set/embedded_file_container.rs b/packages/onenote-converter/src/parser/one/property_set/embedded_file_container.rs new file mode 100644 index 00000000000..f52ab5ccb43 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property_set/embedded_file_container.rs @@ -0,0 +1,35 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property_set::PropertySetId; +use crate::parser::onestore::object::Object; + +/// An embedded file data container. +/// +/// See [\[MS-ONE\] 2.2.59]. +/// +/// [\[MS-ONE\] 2.2.59]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/e2a23dc5-75a5-407f-b5ff-d3412379fa7b +#[derive(Debug)] +pub(crate) struct Data(pub(crate) Vec); + +impl Data { + pub(crate) fn into_value(self) -> Vec { + self.0 + } +} + +pub(crate) fn parse(object: &Object) -> Result { + if object.id() != PropertySetId::EmbeddedFileContainer.as_jcid() { + return Err(ErrorKind::MalformedOneNoteFileData( + format!("unexpected object type: 0x{:X}", object.id().0).into(), + ) + .into()); + } + + let data = object + .file_data() + .ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("embedded file container has no data".into()) + })? + .to_vec(); + + Ok(Data(data)) +} diff --git a/packages/onenote-converter/src/parser/one/property_set/embedded_file_node.rs b/packages/onenote-converter/src/parser/one/property_set/embedded_file_node.rs new file mode 100644 index 00000000000..3c50308a6bd --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property_set/embedded_file_node.rs @@ -0,0 +1,106 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::fsshttpb::data::exguid::ExGuid; +use crate::parser::one::property::file_type::FileType; +use crate::parser::one::property::layout_alignment::LayoutAlignment; +use crate::parser::one::property::object_reference::ObjectReference; +use crate::parser::one::property::time::Time; +use crate::parser::one::property::{simple, PropertyType}; +use crate::parser::one::property_set::note_tag_container::Data as NoteTagData; +use crate::parser::one::property_set::PropertySetId; +use crate::parser::onestore::object::Object; +use crate::utils::utils::log_warn; + +/// An embedded file. +/// +/// See [\[MS-ONE\] 2.2.32]. +/// +/// [\[MS-ONE\] 2.2.32]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/a665b5ad-ff40-4c0c-9e42-4b707254dc3f +#[derive(Debug)] +pub(crate) struct Data { + pub(crate) last_modified: Time, + pub(crate) picture_container: Option, + pub(crate) layout_max_width: Option, + pub(crate) layout_max_height: Option, + pub(crate) is_layout_size_set_by_user: bool, + pub(crate) text: Option, + pub(crate) text_language_code: Option, + pub(crate) layout_alignment_in_parent: Option, + pub(crate) layout_alignment_self: Option, + pub(crate) embedded_file_container: ExGuid, + pub(crate) embedded_file_name: String, + pub(crate) source_path: Option, + pub(crate) file_type: FileType, + pub(crate) picture_width: Option, + pub(crate) picture_height: Option, + pub(crate) note_tags: Vec, + pub(crate) offset_from_parent_horiz: Option, + pub(crate) offset_from_parent_vert: Option, + pub(crate) recording_duration: Option, +} + +pub(crate) fn parse(object: &Object) -> Result { + if object.id() != PropertySetId::EmbeddedFileNode.as_jcid() { + return Err(ErrorKind::MalformedOneNoteFileData( + format!("unexpected object type: 0x{:X}", object.id().0).into(), + ) + .into()); + } + + let last_modified = Time::parse(PropertyType::LastModifiedTime, object)?.ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("embedded file has no last modified time".into()) + })?; + let picture_container = ObjectReference::parse(PropertyType::PictureContainer, object)?; + let layout_max_width = simple::parse_f32(PropertyType::LayoutMaxWidth, object)?; + let layout_max_height = simple::parse_f32(PropertyType::LayoutMaxHeight, object)?; + let is_layout_size_set_by_user = + simple::parse_bool(PropertyType::IsLayoutSizeSetByUser, object)?.unwrap_or_default(); + let text = simple::parse_string(PropertyType::RichEditTextUnicode, object)?; + let text_language_code = + simple::parse_u16(PropertyType::RichEditTextLangId, object)?.map(|value| value as u32); + let layout_alignment_in_parent = + LayoutAlignment::parse(PropertyType::LayoutAlignmentInParent, object)?; + let layout_alignment_self = LayoutAlignment::parse(PropertyType::LayoutAlignmentSelf, object)?; + let embedded_file_container = + ObjectReference::parse(PropertyType::EmbeddedFileContainer, object)?.ok_or_else(|| { + log_warn!("embeded file has no file container"); + ErrorKind::MalformedOneNoteFileData("embedded file has no file container".into()) + })?; + + let embedded_file_name = simple::parse_string(PropertyType::EmbeddedFileName, object)? + .ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData("embedded file has no file name".into()) + })?; + let source_path = simple::parse_string(PropertyType::SourceFilepath, object)?; + let file_type = FileType::parse(object)?; + let picture_width = simple::parse_f32(PropertyType::PictureWidth, object)?; + let picture_height = simple::parse_f32(PropertyType::PictureHeight, object)?; + let offset_from_parent_horiz = simple::parse_f32(PropertyType::OffsetFromParentHoriz, object)?; + let offset_from_parent_vert = simple::parse_f32(PropertyType::OffsetFromParentVert, object)?; + // let recording_duration = simple::parse_u32(PropertyType::Duration) // FIXME: Record duration property id not known + + let note_tags = NoteTagData::parse(object)?.unwrap_or_default(); + + let data = Data { + last_modified, + picture_container, + layout_max_width, + layout_max_height, + is_layout_size_set_by_user, + text, + text_language_code, + layout_alignment_in_parent, + layout_alignment_self, + embedded_file_container, + embedded_file_name, + source_path, + file_type, + picture_width, + picture_height, + note_tags, + offset_from_parent_horiz, + offset_from_parent_vert, + recording_duration: None, // FIXME: Parse this + }; + + Ok(data) +} diff --git a/packages/onenote-converter/src/parser/one/property_set/embedded_ink_container.rs b/packages/onenote-converter/src/parser/one/property_set/embedded_ink_container.rs new file mode 100644 index 00000000000..735cad2434b --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property_set/embedded_ink_container.rs @@ -0,0 +1,121 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::object_reference::ObjectReference; +use crate::parser::one::property::object_space_reference::ObjectSpaceReference; +use crate::parser::one::property::{simple, PropertyType}; +use crate::parser::onestore::object::Object; +use crate::parser::onestore::types::compact_id::CompactId; +use crate::parser::onestore::types::jcid::JcId; +use crate::parser::onestore::types::object_prop_set::ObjectPropSet; +use crate::parser::onestore::types::prop_set::PropertySet; +use crate::parser::onestore::types::property::PropertyId; + +/// An embedded ink handwriting container. +#[derive(Debug)] +pub(crate) struct Data { + pub(crate) space_width: Option, + pub(crate) space_height: Option, + + pub(crate) start_x: Option, + pub(crate) start_y: Option, + pub(crate) height: Option, + pub(crate) width: Option, + pub(crate) offset_horiz: Option, + pub(crate) offset_vert: Option, +} + +impl Data { + pub(crate) fn parse(object: &Object) -> Result>> { + let (prop_id, prop_sets) = match object.props().get(PropertyType::TextRunData) { + Some(value) => value.to_property_values().ok_or_else(|| { + ErrorKind::MalformedOneNoteFileData( + "embedded ink container is not a property values list".into(), + ) + })?, + None => return Ok(None), + }; + + let data = prop_sets + .iter() + .map(|props| { + let object = Self::parse_object(object, prop_id, props)?; + let data = Self::parse_data(object)?; + + Ok(data) + }) + .collect::>>()?; + + Ok(Some(data)) + } + + fn parse_object<'a>( + object: &'a Object, + prop_id: PropertyId, + props: &PropertySet, + ) -> Result> { + Ok(Object { + context_id: object.context_id, + jc_id: JcId(prop_id.value()), + props: ObjectPropSet { + object_ids: Self::get_object_ids(props, object)?, + object_space_ids: Self::get_object_space_ids(props, object)?, + context_ids: vec![], + properties: props.clone(), + }, + file_data: None, + mapping: object.mapping.clone(), + }) + } + + fn parse_data(object: Object) -> Result { + let space_width = simple::parse_f32(PropertyType::EmbeddedInkSpaceWidth, &object)?; + let space_height = simple::parse_f32(PropertyType::EmbeddedInkSpaceHeight, &object)?; + + let start_x = simple::parse_f32(PropertyType::EmbeddedInkStartX, &object)?; + let start_y = simple::parse_f32(PropertyType::EmbeddedInkStartY, &object)?; + let height = simple::parse_f32(PropertyType::EmbeddedInkHeight, &object)?; + let width = simple::parse_f32(PropertyType::EmbeddedInkWidth, &object)?; + let offset_horiz = simple::parse_f32(PropertyType::EmbeddedInkOffsetHoriz, &object)?; + let offset_vert = simple::parse_f32(PropertyType::EmbeddedInkOffsetVert, &object)?; + + let data = Data { + space_width, + space_height, + start_x, + start_y, + height, + width, + offset_horiz, + offset_vert, + }; + + Ok(data) + } + + fn get_object_ids(props: &PropertySet, object: &Object) -> Result> { + Ok(object + .props + .object_ids + .iter() + .skip(ObjectReference::get_offset( + PropertyType::TextRunData, + object, + )?) + .take(ObjectReference::count_references(props.values())) + .copied() + .collect()) + } + + fn get_object_space_ids(props: &PropertySet, object: &Object) -> Result> { + Ok(object + .props + .object_ids + .iter() + .skip(ObjectSpaceReference::get_offset( + PropertyType::TextRunData, + object, + )?) + .take(ObjectSpaceReference::count_references(props.values())) + .copied() + .collect()) + } +} diff --git a/packages/onenote-converter/src/parser/one/property_set/iframe_node.rs b/packages/onenote-converter/src/parser/one/property_set/iframe_node.rs new file mode 100644 index 00000000000..cf45edb5563 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property_set/iframe_node.rs @@ -0,0 +1,28 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::one::property::{simple, PropertyType}; +use crate::parser::one::property_set::PropertySetId; +use crate::parser::onestore::object::Object; + +/// An ink data container. +pub(crate) struct Data { + pub(crate) embed_type: Option, + pub(crate) source_url: String, +} + +pub(crate) fn parse(object: &Object) -> Result { + if object.id() != PropertySetId::IFrameNode.as_jcid() { + return Err(ErrorKind::MalformedOneNoteFileData( + format!("unexpected object type: 0x{:X}", object.id().0).into(), + ) + .into()); + } + + let embed_type = simple::parse_u32(PropertyType::ImageEmbedType, object)?; + let source_url = simple::parse_string(PropertyType::ImageEmbeddedUrl, object)? + .ok_or_else(|| ErrorKind::MalformedOneNoteFileData("iframe has no source URL".into()))?; + + Ok(Data { + embed_type, + source_url, + }) +} diff --git a/packages/onenote-converter/src/parser/one/property_set/image_node.rs b/packages/onenote-converter/src/parser/one/property_set/image_node.rs new file mode 100644 index 00000000000..e69af0b776c --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property_set/image_node.rs @@ -0,0 +1,107 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::fsshttpb::data::exguid::ExGuid; +use crate::parser::one::property::layout_alignment::LayoutAlignment; +use crate::parser::one::property::object_reference::ObjectReference; +use crate::parser::one::property::{simple, PropertyType}; +use crate::parser::one::property_set::note_tag_container::Data as NoteTagData; +use crate::parser::one::property_set::PropertySetId; +use crate::parser::onestore::object::Object; + +/// An embedded image. +/// +/// See [\[MS-ONE\] 2.2.24]. +/// +/// [\[MS-ONE\] 2.2.24]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/b7bb4d1a-2a57-4819-9eb4-5a2ce8cf210f +#[derive(Debug)] +pub(crate) struct Data { + // pub(crate) last_modified: Time, + pub(crate) picture_container: Option, + pub(crate) layout_max_width: Option, + pub(crate) layout_max_height: Option, + pub(crate) is_layout_size_set_by_user: bool, + pub(crate) language_code: Option, + pub(crate) alt_text: Option, + pub(crate) layout_alignment_in_parent: Option, + pub(crate) layout_alignment_self: Option, + pub(crate) image_filename: Option, + pub(crate) displayed_page_number: Option, + pub(crate) text: Option, + pub(crate) text_language_code: Option, + pub(crate) picture_width: Option, + pub(crate) picture_height: Option, + pub(crate) hyperlink_url: Option, + pub(crate) note_tags: Vec, + pub(crate) offset_from_parent_horiz: Option, + pub(crate) offset_from_parent_vert: Option, + pub(crate) is_background: bool, + pub(crate) iframe: Vec, +} + +pub(crate) fn parse(object: &Object) -> Result { + if object.id() != PropertySetId::ImageNode.as_jcid() { + return Err(ErrorKind::MalformedOneNoteFileData( + format!("unexpected object type: 0x{:X}", object.id().0).into(), + ) + .into()); + } + + // TODO: add support for last_modified + // let last_modified = Time::parse(PropertyType::LastModifiedTime, object)?.ok_or_else(|| now )?; + // let last_modified = match Time::parse(PropertyType::LastModifiedTime, object) { + // Ok(time) => Ok(time.unwrap()), + // Ok(None) => Ok(now), + // Err(time_err) => Ok(now), + // }?; + let picture_container = ObjectReference::parse(PropertyType::PictureContainer, object)?; + let layout_max_width = simple::parse_f32(PropertyType::LayoutMaxWidth, object)?; + let layout_max_height = simple::parse_f32(PropertyType::LayoutMaxHeight, object)?; + let is_layout_size_set_by_user = + simple::parse_bool(PropertyType::IsLayoutSizeSetByUser, object)?.unwrap_or_default(); + let language_code = simple::parse_u32(PropertyType::LanguageId, object)?; + let alt_text = simple::parse_string(PropertyType::ImageAltText, object)?; + let layout_alignment_in_parent = + LayoutAlignment::parse(PropertyType::LayoutAlignmentInParent, object)?; + let layout_alignment_self = LayoutAlignment::parse(PropertyType::LayoutAlignmentSelf, object)?; + let image_filename = simple::parse_string(PropertyType::ImageFilename, object)?; + let displayed_page_number = simple::parse_u32(PropertyType::DisplayedPageNumber, object)?; + let text = simple::parse_string(PropertyType::RichEditTextUnicode, object)?; + let text_language_code = + simple::parse_u16(PropertyType::RichEditTextLangId, object)?.map(|value| value as u32); + let picture_width = simple::parse_f32(PropertyType::PictureWidth, object)?; + let picture_height = simple::parse_f32(PropertyType::PictureHeight, object)?; + let hyperlink_url = simple::parse_string(PropertyType::WzHyperlinkUrl, object)?; + let offset_from_parent_horiz = simple::parse_f32(PropertyType::OffsetFromParentHoriz, object)?; + let offset_from_parent_vert = simple::parse_f32(PropertyType::OffsetFromParentVert, object)?; + let is_background = simple::parse_bool(PropertyType::IsBackground, object)?.unwrap_or_default(); + + let note_tags = NoteTagData::parse(object)?.unwrap_or_default(); + + let iframe = + ObjectReference::parse_vec(PropertyType::ContentChildNodes, object)?.unwrap_or_default(); + + let data = Data { + // last_modified, + picture_container, + layout_max_width, + layout_max_height, + is_layout_size_set_by_user, + language_code, + alt_text, + layout_alignment_in_parent, + layout_alignment_self, + image_filename, + displayed_page_number, + text, + text_language_code, + picture_width, + picture_height, + hyperlink_url, + note_tags, + offset_from_parent_horiz, + offset_from_parent_vert, + is_background, + iframe, + }; + + Ok(data) +} diff --git a/packages/onenote-converter/src/parser/one/property_set/ink_container.rs b/packages/onenote-converter/src/parser/one/property_set/ink_container.rs new file mode 100644 index 00000000000..e0fb3305777 --- /dev/null +++ b/packages/onenote-converter/src/parser/one/property_set/ink_container.rs @@ -0,0 +1,45 @@ +use crate::parser::errors::{ErrorKind, Result}; +use crate::parser::fsshttpb::data::exguid::ExGuid; +use crate::parser::one::property::object_reference::ObjectReference; +use crate::parser::one::property::time::Time; +use crate::parser::one::property::{simple, PropertyType}; +use crate::parser::one::property_set::PropertySetId; +use crate::parser::onestore::object::Object; + +/// An ink container. +#[allow(dead_code)] +pub(crate) struct Data { + pub(crate) offset_from_parent_horiz: Option, + pub(crate) offset_from_parent_vert: Option, + pub(crate) last_modified: Option