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