Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

codelabs-react

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

codelabs-react - npm Package Compare versions

Comparing version 1.3.3 to 1.4.0

setup-tests.js

7

babel.config.json
{
"presets": [
"@babel/react",
"@babel/preset-env"
]
}
"presets": ["@babel/react", "@babel/preset-env"]
}
{
"name": "codelabs-react",
"version": "1.3.3",
"version": "1.4.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"test": "mocha -r esm **/*.test.js",
"test": "jest",
"storybook": "start-storybook -p 6006",

@@ -21,2 +21,3 @@ "build-storybook": "build-storybook",

"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@storybook/addon-actions": "^6.1.11",

@@ -26,5 +27,9 @@ "@storybook/addon-essentials": "^6.1.11",

"@storybook/react": "^6.1.11",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
"baseui": "^9.106.2",
"esm": "^3.2.25",
"jest": "^26.6.3",
"mocha": "^8.2.1",

@@ -34,6 +39,12 @@ "prettier": "^2.2.1",

"react-dom": "^17.0.1",
"regenerator-runtime": "^0.13.7",
"styletron-engine-atomic": "^1.4.6",
"styletron-react": "^5.2.7"
},
"jest": {
"setupFilesAfterEnv": [
"./setup-tests.js"
]
},
"dependencies": {}
}
# codelabs-react
A bring-your-own-styles React library, that turns [Google-style codelabs](https://github.com/googlecodelabs/tools) into React components for easy customization.
A bring-your-own-styles React library, that turns [Google-style codelabs](https://github.com/googlecodelabs/tools/blob/master/FORMAT-GUIDE.md) into React components for easy customization.
**Under active development. Do not use it in production for now.**
You'll need to get the API response from the Google APIs yourself. This project does not handle authentication to the Google APIs. Check out [this guide](https://developers.google.com/docs/api/quickstart/js) if you need help with it.
**Live demo:**
- [With the default components](https://codelabs-react.netlify.app/?path=/story/codelabs-example--default)
- [With overrides using Base Web](https://codelabs-react.netlify.app/?path=/story/codelabs-example--base-web)
## API

@@ -17,23 +22,47 @@

<Codelabs
// required, response from the google docs api
content={content}
// optional, used for styling
overrides={{
Page,
Header,
SideNavigation,
Content,
Main,
H2,
H3,
H4,
H5,
H6,
Span,
ButtonLink,
Snippet,
Link,
InfoBox,
WarningBox,
CodeBox,
// Layout overrides
Header: ({ title }) => React.Component,
Content: ({ children }) => React.Component,
SideNavigation: ({ items, setPage, currentPage }) => React.Component,
Button: ({ children }) => React.Component,
// Text overrides
Parapgraph: ({ children }) => React.Component,
H1: ({ children }) => React.Component,
H2: ({ children }) => React.Component,
H3: ({ children }) => React.Component,
H4: ({ children }) => React.Component,
H5: ({ children }) => React.Component,
H6: ({ children }) => React.Component,
Parapgraph: ({ children }) => React.Component,
ListItem: ({ children }) => React.Component,
// Info and warning boxes
InfoBox: ({ children }) => React.Component,
WarningBox: ({ children }) => React.Component,
// Link overrides
ButtonLink: ({ children, href }) => React.Component,
Link: ({ children, href }) => React.Component,
// Code containers
// Snippet: single-line
// Box: multi-line
Snippet: ({ children }) => React.Component,
CodeBox: ({ children }) => React.Component,
}}
/>;
```
## Roadmap
Currently, the following features are missing, and will be added in the future:
- [ ] Image support
- [ ] YouTube support
- [ ] Per-step time estimation
- [ ] Feedback links

@@ -97,6 +97,10 @@ import React from "react";

export function Span({ children }) {
return <span>{children}</span>;
export function Parapgraph({ children }) {
return <p>{children}</p>;
}
export function ListItem({ children }) {
return <li>{children}</li>;
}
export function ButtonLink({ children, href }) {

@@ -103,0 +107,0 @@ return (

@@ -1,3 +0,202 @@

import Utils from "./utils";
const infoColor = { red: 0.8509804, green: 0.91764706, blue: 0.827451 };
const warningColor = { red: 0.9882353, green: 0.8980392, blue: 0.8039216 };
function parse(content) {
const title = extractTitle(content);
const headings = extractHeadings(content);
const rawPages = extractPageNodes(content);
const pages = rawPages.map((page) => {
return page.map((node) => {
if (node.paragraph) {
return parseParagraph(node.paragraph);
} else if (node.table) {
return parseTable(node.table);
} else {
return null;
}
});
});
return {
title,
headings,
pages,
};
}
function parseParagraph(paragraph) {
return {
type: getParagraphType(paragraph),
content: paragraph.elements.map((element) => {
// TODO(): add image support
if (element.inlineObjectElement) {
return null;
}
// TODO(): add horizontal rule support
if (element.horizontalRule) {
return null;
}
return {
content: element.textRun.content,
...getElementProperties(element),
...getBold(element),
};
}),
};
}
function parseTable(table) {
const tableType = getTableType(table);
return {
type: tableType,
content:
tableType === "codebox"
? // return an array here as well, so the API is consistent
[parseCode(table.tableRows[0].tableCells[0])]
: parseParagraph(table.tableRows[0].tableCells[0].content[0].paragraph),
};
}
function parseCode(tableCell) {
return tableCell.content.reduce((acc, current) => {
return (acc += current.paragraph.elements[0].textRun.content);
}, "");
}
function getTableType(table) {
try {
if (
table.rows === 1 &&
table.columns === 1 &&
table.tableRows[0].tableCells[0].content[0].paragraph.elements &&
isEqual(
table.tableRows[0].tableCells[0].tableCellStyle.backgroundColor.color
.rgbColor,
infoColor
)
) {
return "infobox";
}
} catch (ex) {
// do nothing
}
try {
if (
table.rows === 1 &&
table.columns === 1 &&
table.tableRows[0].tableCells[0].content[0].paragraph.elements &&
isEqual(
table.tableRows[0].tableCells[0].tableCellStyle.backgroundColor.color
.rgbColor,
warningColor
)
) {
return "warningbox";
}
} catch (ex) {
// do nothing
}
try {
if (
table.rows === 1 &&
table.columns === 1 &&
table.tableRows[0].tableCells[0].content[0].paragraph.elements[0].textRun
.textStyle.weightedFontFamily.fontFamily === "Courier New"
) {
return "codebox";
}
} catch (ex) {
// do nothing
}
return null;
}
function getElementProperties(element) {
return {
...getLinkProperties(element.textRun),
...getButtonLinkProperties(element.textRun),
...getCommandLineSnippet(element.textRun),
};
}
function getParagraphType(paragraph) {
const mapping = {
NORMAL_TEXT: "p",
HEADING_2: "h2",
HEADING_3: "h3",
HEADING_4: "h4",
HEADING_5: "h5",
HEADING_6: "h6",
};
try {
if (paragraph.paragraphStyle.spacingMode === "COLLAPSE_LISTS") {
return "li";
}
} catch (ex) {
// do nothing
}
try {
return mapping[paragraph.paragraphStyle.namedStyleType];
} catch (e) {
// defaults to p
return "p";
}
}
function getLinkProperties(textRun) {
try {
if (
textRun.textStyle &&
textRun.textStyle.foregroundColor &&
textRun.textStyle.link
) {
return {
type: "link",
href: textRun.textStyle.link.url,
};
}
} catch (e) {
return undefined;
}
}
function getButtonLinkProperties(textRun) {
try {
if (
textRun.textStyle &&
textRun.textStyle.backgroundColor &&
textRun.textStyle.link
) {
return {
type: "button-link",
href: textRun.textStyle.link.url,
};
}
} catch (e) {
return undefined;
}
}
function getCommandLineSnippet(textRun) {
try {
if (textRun.textStyle.weightedFontFamily.fontFamily === "Consolas") {
return {
type: "command-line-snippet",
};
}
} catch (e) {
return undefined;
}
}
function extractHeadingNodes(content) {

@@ -12,3 +211,3 @@ return findElements(content, "HEADING_1");

function extractTitle(content) {
return Utils.getParagraphText(extractTitleNode(content));
return getParagraphText(extractTitleNode(content));
}

@@ -18,7 +217,22 @@

const headingNodes = extractHeadingNodes(content);
return headingNodes
.map(Utils.getParagraphText)
.map((header) => header.trim());
return headingNodes.map(getParagraphText).map((header) => header.trim());
}
function isEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (object1[key] !== object2[key]) {
return false;
}
}
return true;
}
function extractPageNodes(content) {

@@ -46,10 +260,14 @@ const headingNodes = extractHeadingNodes(content);

export default {
extractHeadingNodes,
extractHeadings,
function getBold(element) {
return {
bold: !!element.textRun.textStyle.bold,
};
}
extractTitleNode,
extractTitle,
function getParagraphText(node) {
return node.paragraph.elements[0].textRun.content;
}
extractPageNodes,
export default {
parse,
};

@@ -1,25 +0,54 @@

const assert = require("assert");
import React from "react";
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
const { content } = require("../stories/document-1607876232180.json");
import assert from "assert";
import { content } from "../stories/document-1607876232180.json";
import expected from "./__test/expected.json";
import Extract from "./extract";
describe("Extract", () => {
it("finds the title", () => {
const title = Extract.extractTitle(content);
assert.deepStrictEqual(title, "Your First Progressive Web App");
});
import { Default } from "../stories/Codelabs.stories";
it("finds h1 headers", () => {
const headings = Extract.extractHeadings(content);
assert.deepStrictEqual(headings, [
"Introduction",
"Getting set up",
"Establish a baseline",
"Add a web app manifest",
"Provide a basic offline experience",
"Provide a full offline experience",
"Add install experience",
"Congratulations",
]);
});
afterEach(cleanup);
test("Extract parses the document", () => {
const tree = Extract.parse(content);
assert.deepStrictEqual(JSON.parse(JSON.stringify(tree)), expected);
});
test("Page can be navigated using the side navigation", async () => {
render(<Default />);
fireEvent.click(screen.getByText("Getting set up"));
expect(screen.getByText("2. Getting set up")).toBeInTheDocument();
});
test("Page can be navigated using the buttons at the bottom of the page", async () => {
render(<Default />);
// navigate to the next page
fireEvent.click(
screen.getByRole("button", {
name: "Next",
})
);
expect(screen.getByText("2. Getting set up")).toBeInTheDocument();
// navigate to the previous page
fireEvent.click(
screen.getByRole("button", {
name: "Previous",
})
);
expect(screen.getByText("1. Introduction")).toBeInTheDocument();
// now the previous button should be disabled
const button = screen.getByRole("button", { name: "Previous" });
expect(button).toBeDisabled();
});

@@ -14,3 +14,2 @@ import React, { useState } from "react";

H6,
Span,
Button,

@@ -23,6 +22,7 @@ ButtonLink,

CodeBox,
Parapgraph,
ListItem,
} from "./components";
import Extract from "./extract";
import Utils from "./utils";

@@ -47,4 +47,5 @@ // TODO: this function is a mess, need to break it apart

const H6Component = overrides.H6 || H6;
const ParapgraphComponent = overrides.Parapgraph || Parapgraph;
const ListItemComponent = overrides.ListItem || ListItem;
const SpanComponent = overrides.Span || Span;
const ButtonLinkComponent = overrides.ButtonLink || ButtonLink;

@@ -58,128 +59,167 @@ const ButtonComponent = overrides.Button || Button;

const Text = Utils.TextFactory({
H2Component,
H3Component,
H4Component,
H5Component,
H6Component,
SpanComponent,
ButtonLinkComponent,
});
const parsedContent = Extract.parse(content);
const title = Extract.extractTitle(content);
const headings = Extract.extractHeadings(content);
const Mapper = {
p: (props) => <ParapgraphComponent>{props.children}</ParapgraphComponent>,
h2: (props) => <H2Component>{props.children}</H2Component>,
h3: (props) => <H3Component>{props.children}</H3Component>,
h4: (props) => <H4Component>{props.children}</H4Component>,
h5: (props) => <H5Component>{props.children}</H5Component>,
h6: (props) => <H6Component>{props.children}</H6Component>,
li: (props) => <ListItemComponent>{props.children}</ListItemComponent>,
infobox: (props) => <InfoBoxComponent>{props.children}</InfoBoxComponent>,
warningbox: (props) => (
<WarningBoxComponent>{props.children}</WarningBoxComponent>
),
a: (props) => <LinkComponent {...props}>{props.children}</LinkComponent>,
buttonlink: (props) => (
<ButtonLinkComponent {...props}>{props.children}</ButtonLinkComponent>
),
commandlinesnippet: (props) => (
<SnippetComponent {...props}>{props.children}</SnippetComponent>
),
codebox: (props) => (
<CodeBoxComponent {...props}>{props.children}</CodeBoxComponent>
),
};
const pageNodes = Extract.extractPageNodes(content);
const pages = parsedContent.pages.map((page, pageIndex) => {
return page.map((node, nodeIndex) => {
switch (node.type) {
case "p":
return MapNode({
node,
tag: "p",
Mapper,
key: `${pageIndex}-${nodeIndex}`,
});
case "h2":
return MapNode({
node,
tag: "h2",
Mapper,
key: `${pageIndex}-${nodeIndex}`,
});
case "h3":
return MapNode({
node,
tag: "h3",
Mapper,
key: `${pageIndex}-${nodeIndex}`,
});
case "h4":
return MapNode({
node,
tag: "h4",
Mapper,
key: `${pageIndex}-${nodeIndex}`,
});
case "h5":
return MapNode({
node,
tag: "h5",
Mapper,
key: `${pageIndex}-${nodeIndex}`,
});
case "h6":
return MapNode({
node,
tag: "h6",
Mapper,
key: `${pageIndex}-${nodeIndex}`,
});
case "li":
return MapNode({
node,
tag: "li",
Mapper,
key: `${pageIndex}-${nodeIndex}`,
});
case "infobox":
return MapNode({
node: node.content,
tag: "infobox",
Mapper,
key: `${pageIndex}-${nodeIndex}`,
});
case "warningbox":
return MapNode({
node: node.content,
tag: "warningbox",
Mapper,
key: `${pageIndex}-${nodeIndex}`,
});
case "codebox":
return MapNode({
node: node,
tag: "codebox",
Mapper,
key: `${pageIndex}-${nodeIndex}`,
});
function processParagraphElements({ type }) {
return function ({ textRun }) {
if (!textRun) return null;
if (Utils.isButton(textRun)) {
return (
<p>
<ButtonLinkComponent href={textRun.textStyle.link.url}>
{textRun.content}
</ButtonLinkComponent>
</p>
);
default:
return null;
}
});
});
if (Utils.isCommandLineSnippet(textRun)) {
return (
<p>
<SnippetComponent>{textRun.content}</SnippetComponent>
</p>
);
}
return (
<PageComponent
title={parsedContent.title}
navigationItems={parsedContent.headings}
pages={pages}
currentPage={page}
setPage={setPage}
overrides={{
HeaderComponent,
SideNavigationComponent,
ContentComponent,
MainComponent,
ButtonComponent,
H1Component,
}}
/>
);
}
if (Utils.isLink(textRun)) {
return (
<LinkComponent href={textRun.textStyle.link.url}>
{textRun.content}
</LinkComponent>
);
}
function MapNode({ tag, node, Mapper, key }) {
const Tag = Mapper[tag];
if (!Tag) return null;
return (
<Text type={type} text={textRun.content} bold={Utils.isBold(textRun)} />
);
};
if (tag === "codebox") {
return <Mapper.codebox key={key}>{node.content}</Mapper.codebox>;
}
const pages = pageNodes.map((page, index) => {
const reactPage = page.map((node) => {
// we have text node, with possibly multiple elements
if (node.paragraph) {
// we can run into a few special cases based on type or other properties
const type = Utils.getParagraphType(node);
return (
<Tag>
{node.content.map((element) => {
if (!element) return;
const pContent = node.paragraph.elements.map(
processParagraphElements({ type })
);
if (Utils.getParagraphSpacingMode(node) === "COLLAPSE_LISTS") {
if (element.type === "link") {
return (
<ul>
<li>{pContent}</li>
</ul>
<Mapper.a key={key} target="_blank" {...element}>
{element.content}
</Mapper.a>
);
}
return pContent;
}
// we might have a codeblock, info or warning boxes
// they are all tables with the dimension 1x1
if (node.table) {
if (Utils.isInfoBox(node.table)) {
const pContent = node.table.tableRows[0].tableCells[0].content[0].paragraph.elements.map(
processParagraphElements({ type: "NORMAL_TEXT" })
if (element.type === "button-link") {
return (
<Mapper.buttonlink key={key} target="_blank" {...element}>
{element.content}
</Mapper.buttonlink>
);
return <InfoBoxComponent>{pContent}</InfoBoxComponent>;
}
if (Utils.isWarningBox(node.table)) {
const pContent = node.table.tableRows[0].tableCells[0].content[0].paragraph.elements.map(
processParagraphElements({ type: "NORMAL_TEXT" })
);
return <WarningBoxComponent>{pContent}</WarningBoxComponent>;
}
if (Utils.isCodeBox(node.table)) {
if (element.type === "command-line-snippet") {
return (
<CodeBoxComponent>
{Utils.getCode(node.table.tableRows[0].tableCells[0])}
</CodeBoxComponent>
<Mapper.commandlinesnippet key={key}>
{element.content}
</Mapper.commandlinesnippet>
);
}
}
return;
});
// +1, so it's a human readable page index
reactPage.unshift(
<H1Component>
{index + 1}. {headings[index]}
</H1Component>
);
return reactPage;
});
return (
<PageComponent
title={title}
navigationItems={headings}
pages={pages}
currentPage={page}
setPage={setPage}
overrides={{
HeaderComponent,
SideNavigationComponent,
ContentComponent,
MainComponent,
ButtonComponent,
}}
/>
return <span key={key}>{element.content}</span>;
})}
</Tag>
);

@@ -200,2 +240,3 @@ }

ButtonComponent,
H1Component,
},

@@ -214,2 +255,5 @@ }) {

<>
<H1Component>
{currentPage + 1}. {navigationItems[currentPage]}
</H1Component>
{pages[currentPage]}

@@ -216,0 +260,0 @@ <div

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc