@vishu1301/script-writing 0.4.2 → 0.4.3

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/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
2
- import { ArrowRightLeft, MessageCircle, Brackets, UserRound, Sparkles, Clapperboard, ArrowRight, User, ChevronRight, Save, FileDown, Cog } from 'lucide-react';
2
+ import { ArrowRightLeft, MessageCircle, Brackets, UserRound, Sparkles, Clapperboard, ArrowRight, User, ChevronRight, Upload, Save, FileDown, Cog } from 'lucide-react';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import * as pdfjs from 'pdfjs-dist';
4
5
 
5
6
  var __defProp = Object.defineProperty;
6
7
  var __defProps = Object.defineProperties;
@@ -124,6 +125,99 @@ var blockStyles = {
124
125
  }
125
126
  }
126
127
  };
128
+ pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
129
+ function PdfImporter({ onScriptImported, children }) {
130
+ const [isProcessing, setIsProcessing] = useState(false);
131
+ const [error, setError] = useState(null);
132
+ const fileInputRef = useRef(null);
133
+ const handleFileChange = async (event) => {
134
+ var _a;
135
+ const file = (_a = event.target.files) == null ? void 0 : _a[0];
136
+ if (!file) {
137
+ return;
138
+ }
139
+ setIsProcessing(true);
140
+ setError(null);
141
+ try {
142
+ const arrayBuffer = await file.arrayBuffer();
143
+ const pdf = await pdfjs.getDocument(arrayBuffer).promise;
144
+ const processPage = async (pageNumber) => {
145
+ const page = await pdf.getPage(pageNumber);
146
+ const content = await page.getTextContent();
147
+ const items = content.items.filter((item) => "str" in item && item.str.trim().length > 0);
148
+ if (items.length === 0) return "";
149
+ const lines = [];
150
+ for (const item of items) {
151
+ let found = false;
152
+ for (const line of lines) {
153
+ if (Math.abs(line.y - item.transform[5]) < 5) {
154
+ line.items.push({ x: item.transform[4], text: item.str });
155
+ found = true;
156
+ break;
157
+ }
158
+ }
159
+ if (!found) {
160
+ lines.push({ y: item.transform[5], items: [{ x: item.transform[4], text: item.str }] });
161
+ }
162
+ }
163
+ lines.sort((a, b) => b.y - a.y);
164
+ return lines.map((line) => {
165
+ line.items.sort((a, b) => a.x - b.x);
166
+ return line.items.map((item) => item.text).join(" ");
167
+ }).join("\n");
168
+ };
169
+ let title = "";
170
+ if (pdf.numPages > 0) {
171
+ title = await processPage(1);
172
+ }
173
+ let scriptContent = "";
174
+ for (let i = 2; i <= pdf.numPages; i++) {
175
+ scriptContent += await processPage(i) + "\n\n";
176
+ }
177
+ onScriptImported(title.trim(), scriptContent);
178
+ } catch (err) {
179
+ console.error("Error processing PDF:", err);
180
+ setError(err instanceof Error ? `Error processing PDF: ${err.message}` : "An unknown error occurred.");
181
+ } finally {
182
+ setIsProcessing(false);
183
+ if (event.target) {
184
+ event.target.value = "";
185
+ }
186
+ }
187
+ };
188
+ const handleClick = () => {
189
+ var _a;
190
+ (_a = fileInputRef.current) == null ? void 0 : _a.click();
191
+ };
192
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
193
+ /* @__PURE__ */ jsx(
194
+ "input",
195
+ {
196
+ ref: fileInputRef,
197
+ type: "file",
198
+ accept: "application/pdf",
199
+ onChange: handleFileChange,
200
+ disabled: isProcessing,
201
+ className: "hidden",
202
+ id: "pdf-importer-input"
203
+ }
204
+ ),
205
+ /* @__PURE__ */ jsx(
206
+ "button",
207
+ {
208
+ onClick: handleClick,
209
+ disabled: isProcessing,
210
+ 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",
211
+ "aria-label": "Import Script from PDF",
212
+ children: isProcessing ? /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: "Processing..." }) : children
213
+ }
214
+ ),
215
+ error && /* @__PURE__ */ jsxs("p", { className: "sr-only", children: [
216
+ "Error: ",
217
+ error
218
+ ] })
219
+ ] });
220
+ }
127
221
  function ScreenplayEditorView({
128
222
  blocks,
129
223
  pages,
@@ -145,9 +239,11 @@ function ScreenplayEditorView({
145
239
  handleKeyDown,
146
240
  handleFocus,
147
241
  handleBlur,
242
+ handleScriptImport,
148
243
  onSave,
149
244
  onSaveAsPdf,
150
- onSyncWithCloud
245
+ onSyncWithCloud,
246
+ handleSceneNumberChange
151
247
  }) {
152
248
  const [isRulesOpen, setIsRulesOpen] = useState(false);
153
249
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -206,7 +302,26 @@ function ScreenplayEditorView({
206
302
  className: `relative rounded-sm transition-all duration-200 outline-none ${focusedBlockId === block.id ? "bg-zinc-100/50" : "bg-transparent"}`,
207
303
  children: block.type === "SCENE_HEADING" ? /* @__PURE__ */ jsxs(Fragment, { children: [
208
304
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-1 bg-transparent", children: [
209
- /* @__PURE__ */ jsx("span", { className: "absolute -left-16 top-2 w-12 text-right text-zinc-400 font-semibold select-none", children: String(sceneNumbers[block.id] || 0) }),
305
+ /* @__PURE__ */ jsx(
306
+ "input",
307
+ {
308
+ className: "absolute -left-16 top-2 w-12 text-right text-zinc-400 font-semibold select-none bg-transparent outline-none focus:ring-1 focus:ring-blue-400 rounded-sm",
309
+ spellCheck: false,
310
+ value: block.sceneNumber || "",
311
+ onChange: (e) => handleSceneNumberChange(
312
+ block.id,
313
+ e.target.value.toUpperCase()
314
+ ),
315
+ onFocus: () => handleFocus(block.id),
316
+ onBlur: () => handleBlur(block.id),
317
+ onKeyDown: (e) => {
318
+ if (e.key === "Enter" || e.key === "Backspace") {
319
+ e.stopPropagation();
320
+ }
321
+ },
322
+ "aria-label": "Scene Number"
323
+ }
324
+ ),
210
325
  /* @__PURE__ */ jsxs(
211
326
  "select",
212
327
  {
@@ -399,6 +514,10 @@ function ScreenplayEditorView({
399
514
  pageIndex
400
515
  )) }),
401
516
  /* @__PURE__ */ jsxs("div", { className: "fixed bottom-6 right-6 flex flex-col items-end gap-4 z-50", children: [
517
+ /* @__PURE__ */ jsx(PdfImporter, { onScriptImported: handleScriptImport, children: /* @__PURE__ */ jsxs(Fragment, { children: [
518
+ /* @__PURE__ */ jsx(Upload, { className: "w-5 h-5" }),
519
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: "Import" })
520
+ ] }) }),
402
521
  onSave && /* @__PURE__ */ jsxs(
403
522
  "button",
404
523
  {
@@ -501,6 +620,7 @@ function createNewBlock(type) {
501
620
  if (type === "SCENE_HEADING") {
502
621
  newBlock.sceneType = "INT.";
503
622
  newBlock.timeOfDay = "DAY";
623
+ newBlock.sceneNumber = "";
504
624
  } else if (type === "PARENTHETICAL") {
505
625
  newBlock.text = "()";
506
626
  }
@@ -508,12 +628,12 @@ function createNewBlock(type) {
508
628
  }
509
629
  function addBlockAfter(blocks, currentBlockId) {
510
630
  const currentIndex = blocks.findIndex((b) => b.id === currentBlockId);
511
- if (currentIndex === -1) {
512
- return { newBlocks: blocks, newBlockId: "" };
513
- }
514
- const currentBlock = blocks[currentIndex];
515
- const nextType = getNextBlockType(currentBlock.type);
631
+ if (currentIndex === -1) return { newBlocks: blocks, newBlockId: "" };
632
+ const nextType = getNextBlockType(blocks[currentIndex].type);
516
633
  const newBlock = createNewBlock(nextType);
634
+ if (nextType === "SCENE_HEADING") {
635
+ newBlock.sceneNumber = generateNextSceneNumber(blocks, currentIndex);
636
+ }
517
637
  const newBlocks = [
518
638
  ...blocks.slice(0, currentIndex + 1),
519
639
  newBlock,
@@ -538,10 +658,142 @@ function deleteBlock(blocks, blockIdToDelete) {
538
658
  function updateBlock(blocks, id, key, value) {
539
659
  return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, b), { [key]: value }) : b);
540
660
  }
661
+ var generateNextSceneNumber = (blocks, currentIndex) => {
662
+ const prevScenes = blocks.slice(0, currentIndex + 1).filter((b) => b.type === "SCENE_HEADING");
663
+ if (prevScenes.length === 0) return "1";
664
+ const lastScene = prevScenes[prevScenes.length - 1];
665
+ const lastNum = lastScene.sceneNumber || "1";
666
+ const match = lastNum.match(/^(\d+)([A-Z]*)$/);
667
+ if (!match) return "1";
668
+ const baseNumber = match[1];
669
+ const currentSuffix = match[2];
670
+ const scenesAfter = blocks.slice(currentIndex + 1).filter((b) => b.type === "SCENE_HEADING");
671
+ if (scenesAfter.length > 0) {
672
+ if (currentSuffix === "") {
673
+ return `${baseNumber}A`;
674
+ } else {
675
+ return `${baseNumber}${incrementChar(currentSuffix)}`;
676
+ }
677
+ }
678
+ return String(parseInt(baseNumber) + 1);
679
+ };
680
+ function incrementChar(text) {
681
+ if (text === "") return "A";
682
+ const lastChar = text.slice(-1);
683
+ const remaining = text.slice(0, -1);
684
+ if (lastChar === "Z") {
685
+ return incrementChar(remaining) + "A";
686
+ }
687
+ return remaining + String.fromCharCode(lastChar.charCodeAt(0) + 1);
688
+ }
541
689
  function changeBlockType(blocks, id, newType) {
690
+ const currentIndex = blocks.findIndex((b) => b.id === id);
691
+ if (currentIndex === -1) return blocks;
542
692
  const newBlock = createNewBlock(newType);
693
+ if (newType === "SCENE_HEADING") {
694
+ newBlock.sceneNumber = generateNextSceneNumber(blocks, currentIndex);
695
+ }
543
696
  return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, newBlock), { id: b.id }) : b);
544
697
  }
698
+ function parseScreenplayText(content) {
699
+ const lines = content.split("\n");
700
+ const blocks = [];
701
+ let lastBlock = null;
702
+ let activeSpeaker = null;
703
+ for (let i = 0; i < lines.length; i++) {
704
+ let originalLine = lines[i];
705
+ let trimmedLine = lines[i].trim().replace(/\s+/g, " ");
706
+ const leadingSpaces = originalLine.search(/\S/);
707
+ if (lines[i].trim() === "") {
708
+ activeSpeaker = null;
709
+ continue;
710
+ }
711
+ const isPageNumber = /^\d+$/.test(trimmedLine) || /^(PAGE|pg\.?)\s?\d+$/i.test(trimmedLine) || /^[\d]+\.$/.test(trimmedLine);
712
+ if (trimmedLine.length === 0 || isPageNumber) continue;
713
+ let currentBlockType = null;
714
+ const isAllUpperCase = trimmedLine === trimmedLine.toUpperCase() && /[A-Z]/.test(trimmedLine);
715
+ const sceneHeadingStartRegex = /^(?:\d+[A-Z]?\.?\s*)?(INT\.?\/EXT\.?|I\/E|INT|EXT|EST\.)\b/i;
716
+ const isTransition = isAllUpperCase && (trimmedLine.endsWith(" TO:") || ["FADE IN:", "FADE OUT.", "CUT TO BLACK."].includes(trimmedLine));
717
+ if (sceneHeadingStartRegex.test(trimmedLine)) {
718
+ currentBlockType = "SCENE_HEADING";
719
+ activeSpeaker = null;
720
+ } else if (isTransition) {
721
+ currentBlockType = "TRANSITION";
722
+ activeSpeaker = null;
723
+ } else if (isAllUpperCase && !trimmedLine.startsWith("(")) {
724
+ let nextLine = "";
725
+ for (let j = i + 1; j < lines.length; j++) {
726
+ const nextTrimmed = lines[j].trim();
727
+ const nextIsPage = /^\d+$/.test(nextTrimmed) || /^(PAGE|pg\.?)\s?\d+$/i.test(nextTrimmed);
728
+ if (nextTrimmed.length > 0 && !nextIsPage) {
729
+ nextLine = nextTrimmed;
730
+ break;
731
+ }
732
+ }
733
+ if (nextLine && (nextLine.startsWith("(") || nextLine !== nextLine.toUpperCase()) || /\s\(.*\)$/.test(trimmedLine)) {
734
+ currentBlockType = "CHARACTER";
735
+ activeSpeaker = trimmedLine;
736
+ } else {
737
+ currentBlockType = "ACTION";
738
+ activeSpeaker = null;
739
+ }
740
+ } else if (trimmedLine.startsWith("(") && trimmedLine.endsWith(")")) {
741
+ currentBlockType = "PARENTHETICAL";
742
+ } else if (activeSpeaker && ((lastBlock == null ? void 0 : lastBlock.type) === "CHARACTER" || (lastBlock == null ? void 0 : lastBlock.type) === "PARENTHETICAL" || (lastBlock == null ? void 0 : lastBlock.type) === "DIALOGUE")) {
743
+ const isLastLineComplete = /[.!?]"?$/.test(lastBlock.text.trim());
744
+ if (leadingSpaces < 15 && (lastBlock == null ? void 0 : lastBlock.type) !== "CHARACTER" && isLastLineComplete) {
745
+ currentBlockType = "ACTION";
746
+ activeSpeaker = null;
747
+ } else {
748
+ currentBlockType = "DIALOGUE";
749
+ }
750
+ } else {
751
+ currentBlockType = "ACTION";
752
+ activeSpeaker = null;
753
+ }
754
+ if (lastBlock && lastBlock.type === currentBlockType && (currentBlockType === "ACTION" || currentBlockType === "DIALOGUE") && !(currentBlockType === "DIALOGUE" && !activeSpeaker)) {
755
+ lastBlock.text += " " + trimmedLine;
756
+ } else {
757
+ const newBlock = createNewBlock(currentBlockType);
758
+ if (currentBlockType === "SCENE_HEADING") {
759
+ let workingLine = trimmedLine;
760
+ const sceneNumMatch = workingLine.match(/^(\d+[A-Z]*)\.?\s+/i);
761
+ if (sceneNumMatch) {
762
+ newBlock.sceneNumber = String(sceneNumMatch[1]).toUpperCase();
763
+ workingLine = workingLine.replace(/^(\d+[A-Z]*)\.?\s+/i, "").trim();
764
+ } else {
765
+ newBlock.sceneNumber = "";
766
+ }
767
+ const typeMatch = workingLine.match(
768
+ /^(INT\.?\/EXT\.?|I\/E|INT|EXT|EST\.)/i
769
+ );
770
+ if (typeMatch) {
771
+ let sType = typeMatch[0].toUpperCase().replace(/[^A-Z/.]/g, "");
772
+ if (!sType.endsWith(".")) sType += ".";
773
+ newBlock.sceneType = sType;
774
+ workingLine = workingLine.substring(typeMatch[0].length).trim();
775
+ }
776
+ const suffixRegex = /[.\-\s]+(DAY|NIGHT|CONTINUOUS|LATER|MORNING|EVENING|DUSK|DAWN|MORN|AFT|SUNSET|SUNRISE)(?:\s+.*)*$/i;
777
+ const suffixMatch = workingLine.match(suffixRegex);
778
+ if (suffixMatch) {
779
+ const rawTime = suffixMatch[1].toUpperCase();
780
+ const nightKeys = ["NIGHT", "EVENING", "DUSK", "SUNSET"];
781
+ newBlock.timeOfDay = nightKeys.includes(rawTime) ? "NIGHT" : "DAY";
782
+ workingLine = workingLine.substring(0, suffixMatch.index).trim();
783
+ } else {
784
+ workingLine = workingLine.replace(/\s+\d+(\s+\d+)*$/, "").trim();
785
+ newBlock.timeOfDay = "DAY";
786
+ }
787
+ newBlock.text = workingLine.replace(/^[-.\s]+/, "").replace(/[-.\s]+$/, "").toUpperCase();
788
+ } else {
789
+ newBlock.text = trimmedLine;
790
+ }
791
+ blocks.push(newBlock);
792
+ lastBlock = newBlock;
793
+ }
794
+ }
795
+ return blocks.length > 0 ? blocks : [createNewBlock("SCENE_HEADING")];
796
+ }
545
797
 
546
798
  // app/hook/use-screenplay-editor.ts
547
799
  var initialBlocks = [
@@ -549,6 +801,7 @@ var initialBlocks = [
549
801
  id: uuid(),
550
802
  type: "SCENE_HEADING",
551
803
  text: "",
804
+ sceneNumber: "1",
552
805
  sceneType: "INT.",
553
806
  timeOfDay: "DAY"
554
807
  }
@@ -627,6 +880,16 @@ function useScreenplayEditor() {
627
880
  () => ["(V.O.)", "(O.S.)", "(O.C.)", "(SUBTITLE)", "(CONT'D)"],
628
881
  []
629
882
  );
883
+ const handleSceneNumberChange = useCallback(
884
+ (id, newNumber) => {
885
+ setBlocks(
886
+ (prevBlocks) => prevBlocks.map(
887
+ (block) => block.id === id ? __spreadProps(__spreadValues({}, block), { sceneNumber: newNumber.toUpperCase() }) : block
888
+ )
889
+ );
890
+ },
891
+ []
892
+ );
630
893
  const locations = useMemo(() => {
631
894
  const locs = blocks.filter((b) => b.type === "SCENE_HEADING" && b.text.trim() !== "").map((b) => b.text.trim().toUpperCase());
632
895
  return [...new Set(locs)];
@@ -644,15 +907,32 @@ function useScreenplayEditor() {
644
907
  }, [blocks]);
645
908
  const sceneNumbers = useMemo(() => {
646
909
  const map = {};
647
- let count = 0;
910
+ let fallbackCount = 0;
648
911
  blocks.forEach((block) => {
649
912
  if (block.type === "SCENE_HEADING") {
650
- count++;
651
- map[block.id] = count;
913
+ if (block.sceneNumber) {
914
+ map[block.id] = block.sceneNumber;
915
+ const base = parseInt(block.sceneNumber);
916
+ if (!isNaN(base)) fallbackCount = Math.max(fallbackCount, base);
917
+ } else {
918
+ fallbackCount++;
919
+ map[block.id] = String(fallbackCount);
920
+ }
652
921
  }
653
922
  });
654
923
  return map;
655
924
  }, [blocks]);
925
+ useCallback(() => {
926
+ let count = 1;
927
+ setBlocks(
928
+ (prev) => prev.map((b) => {
929
+ if (b.type === "SCENE_HEADING") {
930
+ return __spreadProps(__spreadValues({}, b), { sceneNumber: String(count++) });
931
+ }
932
+ return b;
933
+ })
934
+ );
935
+ }, []);
656
936
  useEffect(() => {
657
937
  if (newBlockId && refs.current[newBlockId]) {
658
938
  const block = blocks.find((b) => b.id === newBlockId);
@@ -1055,28 +1335,62 @@ function useScreenplayEditor() {
1055
1335
  },
1056
1336
  [blocks, handleBlockTextChange]
1057
1337
  );
1058
- const handleFocus = useCallback((id) => {
1059
- if (blurTimeout.current) {
1060
- clearTimeout(blurTimeout.current);
1061
- }
1062
- setFocusedBlockId(id);
1063
- const block = blocks.find((b) => b.id === id);
1064
- if ((block == null ? void 0 : block.type) === "CHARACTER") {
1065
- const trimmedText = block.text.trim();
1066
- const openParenIndex = trimmedText.lastIndexOf("(");
1067
- const closeParenIndex = trimmedText.lastIndexOf(")");
1068
- if (openParenIndex !== -1 && openParenIndex > closeParenIndex) {
1069
- setShowExtensionSuggestions(true);
1070
- setShowSuggestions(false);
1338
+ const handleScriptImport = useCallback(
1339
+ (title, content) => {
1340
+ const parsedBlocks = parseScreenplayText(content);
1341
+ if (parsedBlocks.length > 0) {
1342
+ let fallbackCount = 1;
1343
+ const finalizedBlocks = parsedBlocks.map((block) => {
1344
+ if (block.type === "SCENE_HEADING") {
1345
+ if (block.sceneNumber && block.sceneNumber.trim().length > 0) {
1346
+ const isPureNumber = /^\d+$/.test(block.sceneNumber);
1347
+ if (isPureNumber) {
1348
+ fallbackCount = parseInt(block.sceneNumber) + 1;
1349
+ }
1350
+ return block;
1351
+ }
1352
+ return __spreadProps(__spreadValues({}, block), { sceneNumber: String(fallbackCount++) });
1353
+ }
1354
+ return block;
1355
+ });
1356
+ setBlocks(finalizedBlocks);
1357
+ setTimeout(() => {
1358
+ var _a;
1359
+ const firstId = parsedBlocks[0].id;
1360
+ if (firstId && refs.current[firstId]) {
1361
+ setFocusedBlockId(firstId);
1362
+ (_a = refs.current[firstId]) == null ? void 0 : _a.focus();
1363
+ }
1364
+ }, 100);
1365
+ }
1366
+ },
1367
+ [refs]
1368
+ );
1369
+ const handleFocus = useCallback(
1370
+ (id) => {
1371
+ if (blurTimeout.current) {
1372
+ clearTimeout(blurTimeout.current);
1373
+ }
1374
+ setFocusedBlockId(id);
1375
+ const block = blocks.find((b) => b.id === id);
1376
+ if ((block == null ? void 0 : block.type) === "CHARACTER") {
1377
+ const trimmedText = block.text.trim();
1378
+ const openParenIndex = trimmedText.lastIndexOf("(");
1379
+ const closeParenIndex = trimmedText.lastIndexOf(")");
1380
+ if (openParenIndex !== -1 && openParenIndex > closeParenIndex) {
1381
+ setShowExtensionSuggestions(true);
1382
+ setShowSuggestions(false);
1383
+ } else {
1384
+ setShowExtensionSuggestions(false);
1385
+ setShowSuggestions(openParenIndex === -1);
1386
+ }
1071
1387
  } else {
1388
+ setShowSuggestions(true);
1072
1389
  setShowExtensionSuggestions(false);
1073
- setShowSuggestions(openParenIndex === -1);
1074
1390
  }
1075
- } else {
1076
- setShowSuggestions(true);
1077
- setShowExtensionSuggestions(false);
1078
- }
1079
- }, [blocks]);
1391
+ },
1392
+ [blocks]
1393
+ );
1080
1394
  const handleBlur = useCallback((id) => {
1081
1395
  if (document.activeElement === refs.current[id]) return;
1082
1396
  blurTimeout.current = setTimeout(() => {
@@ -1103,6 +1417,8 @@ function useScreenplayEditor() {
1103
1417
  handleBlockTypeChange,
1104
1418
  handleSelectCharacterExtension,
1105
1419
  handleKeyDown,
1420
+ handleScriptImport,
1421
+ handleSceneNumberChange,
1106
1422
  handleFocus,
1107
1423
  handleBlur
1108
1424
  };