Skip to content
This repository has been archived by the owner on Jan 16, 2024. It is now read-only.

Commit

Permalink
feat: Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
connor-baer committed Jun 23, 2019
1 parent a35e5dd commit e3c3cc4
Show file tree
Hide file tree
Showing 9 changed files with 366 additions and 17 deletions.
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<div align="center">

# package-template <!-- omit in TOC -->
# rich-text-from-notion <!-- omit in TOC -->

JS package template repository
A library to convert [Notion](https://notion.so) page content to the Contentful [Rich Text](https://www.contentful.com/developers/docs/concepts/rich-text/) document format.

[![npm version](https://badge.fury.io/js/%40madebyconnor%2Fpackage-template.svg)](https://badge.fury.io/js/%40madebyconnor%2Fpackage-template) [![Build Status](https://travis-ci.org/connor-baer/package-template.svg?branch=master)](https://travis-ci.org/connor-baer/package-template) [![codecov](https://codecov.io/gh/connor-baer/package-template/branch/master/graph/badge.svg)](https://codecov.io/gh/connor-baer/package-template) [![License MIT](https://img.shields.io/github/license/connor-baer/package-template.svg)](https://github.com/connor-baer/package-template/blob/master/LICENSE.md)
[![npm version](https://badge.fury.io/js/%40madebyconnor%2Frich-text-from-notion.svg)](https://badge.fury.io/js/%40madebyconnor%2Frich-text-from-notion) [![Build Status](https://travis-ci.org/connor-baer/rich-text-from-notion.svg?branch=master)](https://travis-ci.org/connor-baer/rich-text-from-notion) [![codecov](https://codecov.io/gh/connor-baer/rich-text-from-notion/branch/master/graph/badge.svg)](https://codecov.io/gh/connor-baer/rich-text-from-notion) [![License MIT](https://img.shields.io/github/license/connor-baer/rich-text-from-notion.svg)](https://github.com/connor-baer/rich-text-from-notion/blob/master/LICENSE.md)

</div>

Expand All @@ -19,38 +19,38 @@ JS package template repository

---

[`package-template`](https://www.npmjs.com/package/@madebyconnor/package-template) is a template repository for my JavaScript modules.
[`rich-text-from-notion`](https://www.npmjs.com/package/@madebyconnor/rich-text-from-notion) is *in writing...*

Some standout features include...

All this clocks in at around kB gzipped.
All this clocks in at around 3.7 kB gzipped.

⚠️ Requires Node >= 8.0.0.

## Installation

Install `@madebyconnor/package-template` with your favorite package manager.
Install `@madebyconnor/rich-text-from-notion` with your favorite package manager.

```shell
# yarn
yarn add @madebyconnor/package-template
yarn add @madebyconnor/rich-text-from-notion
# npm
npm i @madebyconnor/package-template
npm install @madebyconnor/rich-text-from-notion
```

## Getting started

`@madebyconnor/package-template` exports a function
`@madebyconnor/rich-text-from-notion` exports a function

```js
import sayHello from '@madebyconnor/package-template';
import richTextFromNotion from '@madebyconnor/rich-text-from-notion';

sayHello('Tessa')
richTextFromNotion(notionPageContent)

/*
returns:
Hello Tessa
rich text. duh! (in writing...)
*/
```

Expand All @@ -67,4 +67,4 @@ Here are instructions for some of the popular bundlers:

## Changelog

See [GitHub Releases](https://github.com/connor-baer/package-template/releases).
See [GitHub Releases](https://github.com/connor-baer/rich-text-from-notion/releases).
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@madebyconnor/package-template",
"name": "@madebyconnor/rich-text-from-notion",
"version": "0.0.0-semantically-released",
"description": "JS package template",
"sideEffects": false,
Expand All @@ -10,7 +10,7 @@
"files": [
"dist"
],
"repository": "https://github.com/connor-baer/package-template.git",
"repository": "https://github.com/connor-baer/rich-text-from-notion.git",
"author": "(Connor Bär <github@connorbaer.co>)",
"license": "MIT",
"engines": {
Expand Down Expand Up @@ -48,5 +48,8 @@
"rollup-plugin-node-resolve": "^5.0.4",
"rollup-plugin-peer-deps-external": "^2.2.0",
"size-limit": "^1.3.7"
},
"dependencies": {
"@contentful/rich-text-types": "^13.1.0"
}
}
22 changes: 22 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BLOCKS, MARKS as CONTENTFUL_MARKS } from '@contentful/rich-text-types';

export { BLOCKS };

export const MARKS = {
...CONTENTFUL_MARKS,
HIGHLIGHT: 'highlight'
};

export const NOTION_BLOCKS = {
PAGE: 'page',
HEADER: 'header',
SUB_HEADER: 'sub_header',
SUB_SUB_HEADER: 'sub_sub_header',
TEXT: 'text',
QUOTE: 'quote',
BULLETED_LIST: 'bulleted_list',
NUMBERED_LIST: 'numbered_list',
IMAGE: 'image',
DIVIDER: 'divider',
COLLECTION_VIEW: 'collection_view'
};
181 changes: 179 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,180 @@
export default function sayHello(name) {
return `Hello ${name}`;
import { BLOCKS, MARKS, NOTION_BLOCKS } from './constants';
import isEmpty from './utils/is-empty';
import get from './utils/get';

const markMap = {
b: MARKS.BOLD,
i: MARKS.ITALIC,
c: MARKS.CODE,
h: MARKS.HIGHLIGHT
};

function toMark(mark = []) {
const [typeId, data] = mark;
return {
type: markMap[typeId],
data
};
}

function toText(text = []) {
const [value, marks = []] = text;
return {
nodeType: 'text',
value,
marks: marks.map(toMark)
};
}

function toHeading(level) {
return block => {
const content = get(block, 'value.properties.title', []);

if (isEmpty(content)) {
return null;
}

return {
nodeType: `heading-${level}`,
content: content.map(toText)
};
};
}

function toParagraph(block) {
const content = get(block, 'value.properties.title', []);

if (isEmpty(content)) {
return null;
}

return {
nodeType: BLOCKS.PARAGRAPH,
content: content.map(toText)
};
}

function toQuote(block) {
const content = get(block, 'value.properties.title', []);

if (isEmpty(content)) {
return null;
}

return {
nodeType: BLOCKS.QUOTE,
content: content.map(toText)
};
}

function toListItem(listType) {
return block => ({
listType,
nodeType: BLOCKS.LIST_ITEM,
content: [toParagraph(block)]
});
}

function toImage(block) {
const url = get(block, 'value.format.display_source');
const isHostedByNotion = url.startsWith('/images/');
const src = isHostedByNotion ? `https://notion.so${url}` : url;
const caption = get(block, 'value.properties.caption[0]', []);
return {
nodeType: BLOCKS.EMBEDDED_ENTRY,
data: {
type: 'image',
src,
alt: caption[0],
caption: toText(caption)
},
content: []
};
}

function toHorizontalRule() {
return {
content: [],
nodeType: BLOCKS.HR
};
}

const transformerMap = {
[NOTION_BLOCKS.HEADER]: toHeading(2),
[NOTION_BLOCKS.SUB_HEADER]: toHeading(3),
[NOTION_BLOCKS.SUB_SUB_HEADER]: toHeading(4),
[NOTION_BLOCKS.TEXT]: toParagraph,
[NOTION_BLOCKS.QUOTE]: toQuote,
[NOTION_BLOCKS.BULLETED_LIST]: toListItem(BLOCKS.UL_LIST),
[NOTION_BLOCKS.NUMBERED_LIST]: toListItem(BLOCKS.OL_LIST),
[NOTION_BLOCKS.IMAGE]: toImage,
[NOTION_BLOCKS.DIVIDER]: toHorizontalRule
};

function cleanSections(sections) {
const nodes = [];

let tmpListType = null;
let tmpListContent = null;

/* eslint-disable no-restricted-syntax, no-continue */
for (const section of sections) {
if (!section) {
continue;
}

const { listType, ...node } = section;

const isEndOfList = tmpListContent && tmpListType !== listType;

if (isEndOfList) {
nodes.push({
nodeType: tmpListType,
content: tmpListContent
});
tmpListType = null;
tmpListContent = null;
}

const isList = !!listType;

if (isList) {
const isStartOfList = !tmpListType && !tmpListContent;

if (isStartOfList) {
tmpListType = listType;
tmpListContent = [node];
} else {
tmpListContent.push(node);
}

continue;
}

nodes.push(node);
}

return nodes;
}
/* eslint-enable no-restricted-syntax, no-continue */

export default function richTextFromNotion(blocks = {}) {
const sections = Object.values(blocks).map(block => {
const type = get(block, 'value.type');
const transformerFn = transformerMap[type];

if (!transformerFn) {
return null;
}

return transformerFn(block);
});

const content = cleanSections(sections);

return {
nodeType: 'document',
data: {},
content
};
}
37 changes: 37 additions & 0 deletions src/utils/get.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export default function get(obj, path, defaultValue) {
if (!obj || !path) {
return obj;
}
// Get the path as an array
const segments = typeof path !== 'string' ? path : stringToSegments(path);

// Cache the current object
let current = obj;

// For each item in the path, dig into the object
for (let i = 0; i < segments.length; i += 1) {
// If the item isn't found, return the default (or undefined)
if (!current[segments[i]]) {
return defaultValue;
}

// Otherwise, update the current value
current = current[segments[i]];
}

return current;
}

function stringToSegments(path) {
// Split to an array from dot notation
return path.split('.').reduce((allSegments, item) => {
// Split to an array from bracket notation
item.split(/\[([^}]+)\]/g).forEach(key => {
// Push to the new array
if (key.length > 0) {
allSegments.push(key);
}
});
return allSegments;
}, []);
}
49 changes: 49 additions & 0 deletions src/utils/get.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import get from './get';

describe('get', () => {
const obj = {
foo: {
bar: 1,
baz: [
{
fizz: 'buzz'
}
]
}
};

it('should accept the path as an array', () => {
const actual = get(obj, ['foo', 'bar']);
expect(actual).toBe(1);
});

it('should accept the path as a string', () => {
const actual = get(obj, 'foo.bar');
expect(actual).toBe(1);
});

it('should accept the path as a string with bracket notation', () => {
const actual = get(obj, 'foo.baz[0].fizz');
expect(actual).toBe('buzz');
});

it('should return the whole object if no path is passed', () => {
const actual = get(obj);
expect(actual).toBe(obj);
});

it('should return undefined if no object is passed', () => {
const actual = get();
expect(actual).toBeUndefined();
});

it('should return undefined if the path does not exist', () => {
const actual = get(obj, 'fizz.buzz');
expect(actual).toBeUndefined();
});

it('should return the default value if the path does not exist', () => {
const actual = get(obj, 'fizz.buzz', 'default');
expect(actual).toBe('default');
});
});

0 comments on commit e3c3cc4

Please sign in to comment.