Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: disable left area when inspecting, parse table from paste event #24

Merged
merged 3 commits into from
Nov 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"dependencies": {
"@nanostores/solid": "0.2.0",
"classnames": "2.3.1",
"csv-parse": "5.3.2",
"marked": "4.1.0",
"nanostores": "0.6"
},
Expand Down
4 changes: 3 additions & 1 deletion src/components/Button/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@

/* Disabled state. */
.button[disabled=''],
.button[disabled='']:hover {
.button[disabled='']:hover,
.button:disabled,
.button:disabled:hover {
background: theme('colors.slate.400');
border-color: theme('colors.slate.400');
color: theme('colors.slate.200');
Expand Down
1 change: 1 addition & 0 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function Button({ children, class: className, variant, size, isDisabled,
data-size={size}
disabled={isDisabled ? isDisabled() : false}
{...props}
aria-label={props.title}
>
{children}
</button>
Expand Down
43 changes: 36 additions & 7 deletions src/components/Editor/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
inspectContentStore,
InspectStatus,
setInspectContent,
setInspectStatus
setInspectStatus,
inspectStatusStore
} from '../../store/inspect';
import './MarkdownEditor.css';
import { useStore } from '@nanostores/solid';
Expand All @@ -20,17 +21,23 @@ import { modifyTextSelection, Toolbar } from './Toolbar';
import { ToolbarAction } from '../../utils/operators/toolbar';
import { Button } from '../Button';
import { getToolbarHoverText } from './Toolbar/common';
import { parseTableFromTabbedText } from '../../utils/parsers/table';
import {
parseTableFromCommaSeparatedText,
parseTableFromTabbedText
} from '../../utils/parsers/table';

export const MarkdownEditor = () => {
const markdown = useStore(markdownStore);
const editor = useStore(inspectContentStore);
const inspectStatus = useStore(inspectStatusStore);

const [selected, setSelected] = createSignal<[number, number] | undefined>(undefined);
const [prevSelected, setPrevSelected] = createSignal<[number, number] | undefined>(undefined);
const [textAreaElement, setTextAreaElement] = createSignal<HTMLTextAreaElement | undefined>(
undefined
);
const editor = useStore(inspectContentStore);

const [isRawPaste, setIsRawPaste] = createSignal(false);

createEffect<string | undefined>((previous) => {
const rawContent = editor()?.rawContent;
Expand Down Expand Up @@ -81,6 +88,11 @@ export const MarkdownEditor = () => {
};

const onKeyDown: JSX.TextareaHTMLAttributes<HTMLTextAreaElement>['onKeyDown'] = (e) => {
if (e.key.toLowerCase() === 'v' && e.shiftKey && isCtrlOrCmdKey(e)) {
setIsRawPaste(true);
return;
}

const { selectionStart, selectionEnd } = e.currentTarget;

if (e.key === 'Tab' && selectionStart === selectionEnd) {
Expand Down Expand Up @@ -152,7 +164,10 @@ export const MarkdownEditor = () => {
}

return (
<div class="flex flex-col mt-4">
<fieldset
class="flex flex-col mt-4"
disabled={inspectStatus() === InspectStatus.InspectingSnippet}
>
<div class="flex justify-between">
<Toolbar setSelected={setSelected} textAreaElement={textAreaElement} />

Expand Down Expand Up @@ -194,20 +209,34 @@ export const MarkdownEditor = () => {
setMarkdown(nextValue);
}}
onPaste={(e) => {
if (isRawPaste()) {
// Reset the thing that we set on `onKeyDown`.
// Since we don't do prevent default here, it'll passthrough to `onInput`.
setIsRawPaste(false);
return;
}

const pasted = e.clipboardData?.getData('text/plain');
const parseResult = parseTableFromTabbedText(pasted);
// First, try parse from tabbed text.
let parseResult = parseTableFromTabbedText(pasted);
if (!parseResult) {
// If the parse fails, check with comma-separated.
parseResult = parseTableFromCommaSeparatedText(pasted);
console.debug(parseResult);
}

if (parseResult) {
e.preventDefault();
const selectionStart = e.currentTarget.selectionStart;
setMarkdown((prev) =>
prev.slice(0, selectionStart).concat(parseResult).concat(prev.slice(selectionStart))
prev.slice(0, selectionStart).concat(parseResult!).concat(prev.slice(selectionStart))
);

const nextSelectionRange = selectionStart + parseResult.length;
e.currentTarget.setSelectionRange(nextSelectionRange, nextSelectionRange);
}
}}
/>
</div>
</fieldset>
);
};
8 changes: 4 additions & 4 deletions src/components/Editor/SegmentHeading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ interface SegmentHeadingProps {

export function SegmentHeading(props: SegmentHeadingProps) {
return (
<div>
<div class="segment-heading">{props.title}</div>
<div class="segment-subtext">{props.children}</div>
</div>
<>
<h2 class="segment-heading">{props.title}</h2>
<p class="segment-subtext">{props.children}</p>
</>
);
}
37 changes: 23 additions & 14 deletions src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface Props {
}

const { title } = Astro.props as Props;
const origin = (Astro.site?.href || '') + trimSlashes(import.meta.env.BASE_PATH || '')
const origin = (Astro.site?.href || '') + trimSlashes(import.meta.env.BASE_PATH || '');

const date = new Date();
const currentYear = date.getUTCFullYear().toString().padStart(2, '0');
Expand All @@ -21,7 +21,6 @@ let version = 'dev';
if (import.meta.env.VERSION && import.meta.env.GIT_HASH) {
version = `${import.meta.env.VERSION}.${import.meta.env.GIT_HASH}`;
}

---

<!DOCTYPE html>
Expand All @@ -32,22 +31,25 @@ if (import.meta.env.VERSION && import.meta.env.GIT_HASH) {
<link rel="icon" type="image/x-icon" href="/favicon.ico" />

<!-- Primary Meta Tags -->
<meta name="title" content={title}>
<meta name="description" content="Update Markdown tables easier with Markdown Clap.">
<meta name="title" content={title} />
<meta name="description" content="Update Markdown tables easier with Markdown Clap." />

<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content={origin}>
<meta property="og:title" content={title}>
<meta property="og:description" content="Update Markdown tables easier with Markdown Clap.">
<meta property="og:image" content={origin + "/markdownclap-big.png"}>
<meta property="og:type" content="website" />
<meta property="og:url" content={origin} />
<meta property="og:title" content={title} />
<meta property="og:description" content="Update Markdown tables easier with Markdown Clap." />
<meta property="og:image" content={origin + '/markdownclap-big.png'} />

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content={origin}>
<meta property="twitter:title" content={title}>
<meta property="twitter:description" content="Update Markdown tables easier with Markdown Clap.">
<meta property="twitter:image" content={origin + "/markdownclap-big.png"}>
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={origin} />
<meta property="twitter:title" content={title} />
<meta
property="twitter:description"
content="Update Markdown tables easier with Markdown Clap."
/>
<meta property="twitter:image" content={origin + '/markdownclap-big.png'} />

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
Expand Down Expand Up @@ -111,6 +113,13 @@ if (import.meta.env.VERSION && import.meta.env.GIT_HASH) {
border-radius: theme('borderRadius.md');
}

:global(input:disabled),
:global(button:disabled),
:global(textarea:disabled),
:global(select:disabled) {
color: theme('colors.gray.500');
}

:global(input),
:global(select),
:global(textarea) {
Expand Down
8 changes: 4 additions & 4 deletions src/layouts/Main.astro
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ import { SegmentHeading } from '../components/Editor/SegmentHeading';

<div class="p-4">
<div class="editor">
<div class="editor-segment">
<section class="editor-segment">
<SegmentHeading title="Raw content">
Fill input below with Markdown. Currently supported formats are
<Link href="https://github.github.com/gfm/">GFM</Link> and
<Link href="https://commonmark.org">CommonMark</Link>.
</SegmentHeading>

<MarkdownEditor client:visible />
</div>
<div class="editor-segment">
</section>
<section class="editor-segment">
<RightContent client:visible />
</div>
</section>
</div>
</div>

Expand Down
85 changes: 84 additions & 1 deletion src/utils/parsers/table.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { parseTableFromTabbedText } from "./table";
import { parseTableFromCommaSeparatedText, parseTableFromTabbedText } from "./table";

describe('parseTableFromTabbedText', () => {
test('invalid case: undefined', () => {
Expand Down Expand Up @@ -58,4 +58,87 @@ this is a | valid table\they!
|this is a \\| valid table|hey!|
`.trim())
})
})

describe('parseTableFromCommaSeparatedText', () => {
test('invalid case: undefined', () => {
expect(parseTableFromCommaSeparatedText(undefined)).toBe(undefined)
})

test('invalid case: empty string', () => {
expect(parseTableFromCommaSeparatedText('')).toBe(undefined)
})

test('invalid case: no columns', () => {
expect(parseTableFromCommaSeparatedText('test')).toBe(undefined)
})

test('invalid case: different number of columns', () => {
const str = `
hello,world
this is an invalid table
`

expect(parseTableFromCommaSeparatedText(str)).toBe(undefined)
})

test('valid case', () => {
const str = `
hello,world
this is a valid table,hey!
`

expect(parseTableFromCommaSeparatedText(str)).toBe(`
|hello|world|
|this is a valid table|hey!|
`.trim())
})

test('valid case: with trailing newline', () => {
const str = `
hello,world
this is a valid table,hey!
`

expect(parseTableFromCommaSeparatedText(str)).toBe(`
|hello|world|
|this is a valid table|hey!|
`.trim())
})

test('valid case, with pipe characters', () => {
const str = `
hello,world
this is a | valid table,hey!
`

expect(parseTableFromCommaSeparatedText(str)).toBe(`
|hello|world|
|this is a \\| valid table|hey!|
`.trim())
})

test('valid case, with quotes', () => {
const str = `
hello,world
"this is a valid, table",hey!
`

expect(parseTableFromCommaSeparatedText(str)).toBe(`
|hello|world|
|this is a valid, table|hey!|
`.trim())
})

test('valid case, with uneven quotes', () => {
const str = `
hello,world
“this is a valid, table”,hey!
`

expect(parseTableFromCommaSeparatedText(str)).toBe(`
|hello|world|
|this is a valid, table|hey!|
`.trim())
})
})
22 changes: 21 additions & 1 deletion src/utils/parsers/table.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { parse } from 'csv-parse/browser/esm/sync'

export function parseTableFromTabbedText(text: string | undefined): string | undefined {
if (!text) return undefined

Expand All @@ -6,5 +8,23 @@ export function parseTableFromTabbedText(text: string | undefined): string | und
const allLinesHaveEqualTabCount = tabCountPerLine.every(tabCount => tabCount === tabCountPerLine[0] && tabCount > 0)

if (!allLinesHaveEqualTabCount) return undefined
return lines.map(line => `|${line.replace(/\|/g, '\\|').replace(/\t+/g, '|')}|`).join('\n')
return lines.map(line => `|${escapePipes(line).replace(/\t+/g, '|')}|`).join('\n')
}

export function parseTableFromCommaSeparatedText(text: string | undefined): string | undefined {
if (!text) return undefined

try {
const parsed: string[][] = parse(text.trim().replace(/[“”]/g, '"'), { skipEmptyLines: true })
if (parsed.length === 1 && parsed[0].length === 1) return undefined

return parsed.map(line => `|${line.map(column => escapePipes(column)).join('|')}|`).join('\n')
} catch (err) {
return undefined
}
}

// Helper functions.
function escapePipes(line: string) {
return line.replace(/\|/g, '\\|')
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,11 @@ csstype@^3.1.0:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==

[email protected]:
version "5.3.2"
resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.3.2.tgz#a8ce2f8dec1b9c1013c9e73c6102fe0d2d436dbb"
integrity sha512-3jQ/JMs+voKxr4vwpmElS1d37J0o6rQdQyEKoPyA9HG8fYczpLaBJnmp5ykvkXL8ZeEGVP0qwLU645BZVykXKw==

data-uri-to-buffer@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
Expand Down