@vishu1301/script-writing 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/dist/index.cjs +122 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +123 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# Script Writing Editor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An advanced, React and Next.js-based script and screenplay writing component for the web.
|
|
4
4
|
|
|
5
|
-
This component provides writers with an intuitive
|
|
5
|
+
This component provides writers with an intuitive, distraction-free environment to draft, edit, and format their scripts according to industry standards, seamlessly integrated into any React or Next.js application.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- **
|
|
10
|
-
- **Auto-
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
9
|
+
- **Industry-Standard Formatting:** Automatically formats text to screenplay guidelines including Scene Headings, Action, Character, Parenthetical, Dialogue, and Transitions.
|
|
10
|
+
- **Smart Auto-Suggestions:** Intelligently suggests previously used Characters, Locations, and Character Extensions (like V.O., O.S.) as you type.
|
|
11
|
+
- **PDF Import & Export:** Built-in tools to import and parse existing scripts from PDFs (`pdfjs-dist`) and export your drafts into perfectly formatted screenplay PDFs (`jsPDF`).
|
|
12
|
+
- **Scene Management:** Easily toggle Scene Types (INT./EXT.) and Time of Day (DAY/NIGHT) alongside Scene Numbering features.
|
|
13
|
+
- **Keyboard Shortcuts:** Fluid writing experience using keyboard shortcuts (e.g., `Ctrl + ↑/↓` to change block types, `Enter` to create new blocks).
|
|
14
|
+
- **Headless Architecture:** Core logic is exposed via the `useScreenplayEditor` hook, allowing complete customizability over the UI.
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
16
18
|
|
package/dist/index.cjs
CHANGED
|
@@ -161,45 +161,107 @@ function PdfImporter({ onScriptImported, children }) {
|
|
|
161
161
|
setIsProcessing(true);
|
|
162
162
|
setError(null);
|
|
163
163
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
164
|
+
if (file.name.toLowerCase().endsWith(".sbx")) {
|
|
165
|
+
let text = await file.text();
|
|
166
|
+
if (text.includes("<div")) {
|
|
167
|
+
const textarea = document.createElement("textarea");
|
|
168
|
+
textarea.innerHTML = text;
|
|
169
|
+
text = textarea.value;
|
|
170
|
+
}
|
|
171
|
+
const parser = new DOMParser();
|
|
172
|
+
const doc = parser.parseFromString(text, "text/html");
|
|
173
|
+
const divs = Array.from(doc.querySelectorAll("div"));
|
|
174
|
+
const preParsedBlocks = [];
|
|
175
|
+
const typeMap = {
|
|
176
|
+
divtype0: "SCENE_HEADING",
|
|
177
|
+
divtype2: "ACTION",
|
|
178
|
+
divtype3: "CHARACTER",
|
|
179
|
+
divtype4: "PARENTHETICAL",
|
|
180
|
+
divtype5: "DIALOGUE",
|
|
181
|
+
divtype6: "TRANSITION"
|
|
182
|
+
};
|
|
183
|
+
divs.forEach((div) => {
|
|
184
|
+
var _a2;
|
|
185
|
+
let divText = ((_a2 = div.textContent) == null ? void 0 : _a2.trim()) || "";
|
|
186
|
+
if (!divText) return;
|
|
187
|
+
let type = "ACTION";
|
|
188
|
+
for (const className of Array.from(div.classList)) {
|
|
189
|
+
if (typeMap[className]) {
|
|
190
|
+
type = typeMap[className];
|
|
178
191
|
break;
|
|
179
192
|
}
|
|
180
193
|
}
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
const block = { type, text: divText };
|
|
195
|
+
if (type === "SCENE_HEADING") {
|
|
196
|
+
const sceneNum = div.getAttribute("data-scene");
|
|
197
|
+
if (sceneNum) block.sceneNumber = sceneNum;
|
|
198
|
+
let parsedText = divText;
|
|
199
|
+
const typeMatch = parsedText.match(/^(INT\/EXT|INT|EXT)\.?\s+/i);
|
|
200
|
+
if (typeMatch) {
|
|
201
|
+
let sType = typeMatch[1].toUpperCase();
|
|
202
|
+
if (!sType.endsWith(".")) sType += ".";
|
|
203
|
+
block.sceneType = sType;
|
|
204
|
+
parsedText = parsedText.substring(typeMatch[0].length).trim();
|
|
205
|
+
}
|
|
206
|
+
const timeMatch = parsedText.match(/\s+-\s+([^-]+)$/);
|
|
207
|
+
if (timeMatch) {
|
|
208
|
+
block.timeOfDay = timeMatch[1].trim().toUpperCase();
|
|
209
|
+
parsedText = parsedText.substring(0, timeMatch.index).trim();
|
|
210
|
+
}
|
|
211
|
+
block.text = parsedText;
|
|
183
212
|
}
|
|
213
|
+
preParsedBlocks.push(block);
|
|
214
|
+
});
|
|
215
|
+
const title = file.name.replace(/\.sbx$/i, "");
|
|
216
|
+
onScriptImported(title.trim(), "", preParsedBlocks);
|
|
217
|
+
} else {
|
|
218
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
219
|
+
const pdf = await pdfjs__namespace.getDocument(arrayBuffer).promise;
|
|
220
|
+
const processPage = async (pageNumber) => {
|
|
221
|
+
const page = await pdf.getPage(pageNumber);
|
|
222
|
+
const content = await page.getTextContent();
|
|
223
|
+
const items = content.items.filter(
|
|
224
|
+
(item) => "str" in item && item.str.trim().length > 0
|
|
225
|
+
);
|
|
226
|
+
if (items.length === 0) return "";
|
|
227
|
+
const lines = [];
|
|
228
|
+
for (const item of items) {
|
|
229
|
+
let found = false;
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
if (Math.abs(line.y - item.transform[5]) < 5) {
|
|
232
|
+
line.items.push({ x: item.transform[4], text: item.str });
|
|
233
|
+
found = true;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!found) {
|
|
238
|
+
lines.push({
|
|
239
|
+
y: item.transform[5],
|
|
240
|
+
items: [{ x: item.transform[4], text: item.str }]
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
lines.sort((a, b) => b.y - a.y);
|
|
245
|
+
return lines.map((line) => {
|
|
246
|
+
line.items.sort((a, b) => a.x - b.x);
|
|
247
|
+
return line.items.map((item) => item.text).join(" ");
|
|
248
|
+
}).join("\n");
|
|
249
|
+
};
|
|
250
|
+
let title = "";
|
|
251
|
+
if (pdf.numPages > 0) {
|
|
252
|
+
title = await processPage(1);
|
|
184
253
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
191
|
-
let title = "";
|
|
192
|
-
if (pdf.numPages > 0) {
|
|
193
|
-
title = await processPage(1);
|
|
194
|
-
}
|
|
195
|
-
let scriptContent = "";
|
|
196
|
-
for (let i = 2; i <= pdf.numPages; i++) {
|
|
197
|
-
scriptContent += await processPage(i) + "\n\n";
|
|
254
|
+
let scriptContent = "";
|
|
255
|
+
for (let i = 2; i <= pdf.numPages; i++) {
|
|
256
|
+
scriptContent += await processPage(i) + "\n\n";
|
|
257
|
+
}
|
|
258
|
+
onScriptImported(title.trim(), scriptContent);
|
|
198
259
|
}
|
|
199
|
-
onScriptImported(title.trim(), scriptContent);
|
|
200
260
|
} catch (err) {
|
|
201
261
|
console.error("Error processing PDF:", err);
|
|
202
|
-
setError(
|
|
262
|
+
setError(
|
|
263
|
+
err instanceof Error ? `Error processing PDF: ${err.message}` : "An unknown error occurred."
|
|
264
|
+
);
|
|
203
265
|
} finally {
|
|
204
266
|
setIsProcessing(false);
|
|
205
267
|
if (event.target) {
|
|
@@ -217,7 +279,7 @@ function PdfImporter({ onScriptImported, children }) {
|
|
|
217
279
|
{
|
|
218
280
|
ref: fileInputRef,
|
|
219
281
|
type: "file",
|
|
220
|
-
accept: "application/pdf",
|
|
282
|
+
accept: ".pdf,.sbx,application/pdf",
|
|
221
283
|
onChange: handleFileChange,
|
|
222
284
|
disabled: isProcessing,
|
|
223
285
|
className: "hidden",
|
|
@@ -230,7 +292,7 @@ function PdfImporter({ onScriptImported, children }) {
|
|
|
230
292
|
onClick: handleClick,
|
|
231
293
|
disabled: isProcessing,
|
|
232
294
|
className: "flex items-center justify-center gap-2 w-auto px-4 h-12 rounded-full bg-zinc-950 text-white shadow-xl shadow-zinc-900/20 border border-white/10 hover:bg-zinc-800 hover:scale-105 active:scale-95 transition-all duration-300",
|
|
233
|
-
"aria-label": "Import Script
|
|
295
|
+
"aria-label": "Import Script",
|
|
234
296
|
children: isProcessing ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold", children: "Processing..." }) : children
|
|
235
297
|
}
|
|
236
298
|
),
|
|
@@ -249,7 +311,6 @@ function ScreenplayEditorView({
|
|
|
249
311
|
characterExtensions,
|
|
250
312
|
locations,
|
|
251
313
|
characters,
|
|
252
|
-
sceneNumbers,
|
|
253
314
|
handleBlockTextChange,
|
|
254
315
|
handleSceneTypeChange,
|
|
255
316
|
handleTimeOfDayChange,
|
|
@@ -261,6 +322,7 @@ function ScreenplayEditorView({
|
|
|
261
322
|
handleScriptImport,
|
|
262
323
|
onSave,
|
|
263
324
|
onSaveAsPdf,
|
|
325
|
+
onSaveAsSbx,
|
|
264
326
|
onSyncWithCloud,
|
|
265
327
|
handleSceneNumberChange
|
|
266
328
|
}) {
|
|
@@ -554,6 +616,18 @@ function ScreenplayEditorView({
|
|
|
554
616
|
]
|
|
555
617
|
}
|
|
556
618
|
),
|
|
619
|
+
onSaveAsSbx && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
620
|
+
"button",
|
|
621
|
+
{
|
|
622
|
+
onClick: onSaveAsSbx,
|
|
623
|
+
className: "flex items-center justify-center gap-2 w-auto px-4 h-12 rounded-full bg-zinc-950 text-white shadow-xl shadow-zinc-900/20 border border-white/10 hover:bg-zinc-800 hover:scale-105 active:scale-95 transition-all duration-300",
|
|
624
|
+
"aria-label": "Save Script as SBX",
|
|
625
|
+
children: [
|
|
626
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCcw, { className: "w-5 h-5" }),
|
|
627
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold", children: "Save" })
|
|
628
|
+
]
|
|
629
|
+
}
|
|
630
|
+
),
|
|
557
631
|
isRulesOpen && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-white/80 backdrop-blur-md rounded-xl shadow-lg border border-zinc-200/50 p-4 text-xs text-zinc-700 select-none font-sans overflow-hidden transition-all duration-300 w-64 origin-bottom-right animate-in fade-in zoom-in-95", children: [
|
|
558
632
|
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "font-bold text-zinc-800 mb-3 text-sm", children: "Settings & Shortcuts" }),
|
|
559
633
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-4", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5", children: /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "space-y-1.5", children: [
|
|
@@ -907,17 +981,6 @@ function useScreenplayEditor() {
|
|
|
907
981
|
});
|
|
908
982
|
return map;
|
|
909
983
|
}, [blocks]);
|
|
910
|
-
react.useCallback(() => {
|
|
911
|
-
let count = 1;
|
|
912
|
-
setBlocks(
|
|
913
|
-
(prev) => prev.map((b) => {
|
|
914
|
-
if (b.type === "SCENE_HEADING") {
|
|
915
|
-
return __spreadProps(__spreadValues({}, b), { sceneNumber: String(count++) });
|
|
916
|
-
}
|
|
917
|
-
return b;
|
|
918
|
-
})
|
|
919
|
-
);
|
|
920
|
-
}, []);
|
|
921
984
|
react.useEffect(() => {
|
|
922
985
|
if (newBlockId && refs.current[newBlockId]) {
|
|
923
986
|
const block = blocks.find((b) => b.id === newBlockId);
|
|
@@ -1227,8 +1290,20 @@ function useScreenplayEditor() {
|
|
|
1227
1290
|
[blocks, handleBlockTextChange]
|
|
1228
1291
|
);
|
|
1229
1292
|
const handleScriptImport = react.useCallback(
|
|
1230
|
-
(title, content) => {
|
|
1231
|
-
|
|
1293
|
+
(title, content, preParsedBlocks) => {
|
|
1294
|
+
let parsedBlocks = [];
|
|
1295
|
+
if (preParsedBlocks && preParsedBlocks.length > 0) {
|
|
1296
|
+
parsedBlocks = preParsedBlocks.map((b) => ({
|
|
1297
|
+
id: uuid(),
|
|
1298
|
+
type: b.type || "ACTION",
|
|
1299
|
+
text: b.text || "",
|
|
1300
|
+
sceneNumber: b.sceneNumber,
|
|
1301
|
+
sceneType: b.sceneType,
|
|
1302
|
+
timeOfDay: b.timeOfDay
|
|
1303
|
+
}));
|
|
1304
|
+
} else {
|
|
1305
|
+
parsedBlocks = parseScreenplayText(content);
|
|
1306
|
+
}
|
|
1232
1307
|
if (parsedBlocks.length > 0) {
|
|
1233
1308
|
let fallbackCount = 1;
|
|
1234
1309
|
const finalizedBlocks = parsedBlocks.map((block) => {
|