@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 CHANGED
@@ -1,16 +1,18 @@
1
1
  # Script Writing Editor
2
2
 
3
- A modern, React-based script and screenplay writing component for the web.
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 and distraction-free environment to draft, edit, and format their scripts seamlessly within any React or Next.js application.
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
- - **Distraction-Free Editor:** Focus on your story without cluttered UI.
10
- - **Auto-Formatting:** Automatically formats text to industry-standard screenplay guidelines (e.g., Scene Headings, Action, Character, Dialogue).
11
- - **Headless Logic:** Core editor logic (`useScreenplayEditor` hook) is separated from the view for customizability.
12
- - **Responsive Design:** Write on the go, works across desktop and mobile devices.
13
- - **Event-Driven:** Provides callbacks to handle saving, exporting to PDF, or syncing with the cloud.
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
- const arrayBuffer = await file.arrayBuffer();
165
- const pdf = await pdfjs__namespace.getDocument(arrayBuffer).promise;
166
- const processPage = async (pageNumber) => {
167
- const page = await pdf.getPage(pageNumber);
168
- const content = await page.getTextContent();
169
- const items = content.items.filter((item) => "str" in item && item.str.trim().length > 0);
170
- if (items.length === 0) return "";
171
- const lines = [];
172
- for (const item of items) {
173
- let found = false;
174
- for (const line of lines) {
175
- if (Math.abs(line.y - item.transform[5]) < 5) {
176
- line.items.push({ x: item.transform[4], text: item.str });
177
- found = true;
164
+ if (file.name.toLowerCase().endsWith(".sbx")) {
165
+ let text = await file.text();
166
+ if (text.includes("&lt;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
- if (!found) {
182
- lines.push({ y: item.transform[5], items: [{ x: item.transform[4], text: item.str }] });
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
- lines.sort((a, b) => b.y - a.y);
186
- return lines.map((line) => {
187
- line.items.sort((a, b) => a.x - b.x);
188
- return line.items.map((item) => item.text).join(" ");
189
- }).join("\n");
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(err instanceof Error ? `Error processing PDF: ${err.message}` : "An unknown error occurred.");
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 from PDF",
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
- const parsedBlocks = parseScreenplayText(content);
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) => {