Issue
This is my first time writing here, on Stack Overflow, and I am asking for your help in coming to grips with a problem.
The problem I'm going to submit to you is related to the development of the MVP of my new startup that deals with developing SaaS for the creative industry. The MVP in question is a text editor for writing screenplays and I am having a problem.
If you don't know a screenplay has a number of features including the scene title is made like this: "1. EXT. PUB - NIGHT"
Keeping in mind that I was able to do the following:
- make each paragraph autonomous,
- create a function that automatically detects the format based on a set of keywords,
- when the algorithm is active make all text in the paragraph uppercase;
import { Extension } from '@tiptap/core';
import { Plugin } from 'prosemirror-state';
import { Fragment } from 'prosemirror-model';
let paragraphCount = 0; // Variable to track paragraph count
const SceneTitle = Extension.create({
name: 'SceneTitle',
addProseMirrorPlugins() {
return [
new Plugin({
appendTransaction: (_, oldState, newState) => {
let tr = newState.tr;
let oldParagraphCount = 0;
let newParagraphCount = 0;
const regex = /^(INT\.|EXT\.|int\.|ext\.|INT|EXT|int|ext|EST|EST\.|est|est\.)/;
oldState.doc.descendants((node) => {
if (node.type.name === 'paragraph') {
oldParagraphCount++;
}
});
newState.doc.descendants((node, pos) => {
if (node.type.name === 'paragraph') {
newParagraphCount++;
if (regex.test(node.textContent)) {
let from = pos + 1;
let to = from + node.textContent.length;
let fragment = Fragment.from(newState.schema.text(node.textContent.toUpperCase()));
tr.replaceWith(from, to, fragment);
}
}
});
if (newParagraphCount > oldParagraphCount) {
paragraphCount++;
console.log(`Paragraph inserted. Total paragraphs: ${paragraphCount}`); // Print paragraph count
} else if (newParagraphCount < oldParagraphCount) {
paragraphCount = 0;
console.log(`Paragraphs deleted. Paragraph count reset to zero.`); // Print paragraph count reset
}
return tr;
},
}),
];
},
});
export default SceneTitle;
Next, when I enter the numbering of the scene titles it happens that through the creation of an index I am able to divide the paragraph into three parts, namely: the numbering, the text that contains the keyword and the rest of the text. Only when I go to write the keyword, as "Int" it happens that it makes me capitalize only the keyword and not also the text that follows it, however that has not been written yet, if instead I write first the text that will go after the keyword and then write the keyword, as "Int" it makes me uppercase everything.
import { Extension } from '@tiptap/core';
import { Plugin } from 'prosemirror-state';
import { Fragment } from 'prosemirror-model';
let paragraphCount = 0; // Variable to keep track of paragraph count
let sceneCount = 1; // Variable to track scene count
let appliedPositions = new Set(); // Together to track applied positions
const SceneTitle = Extension.create({
name: 'SceneTitle',
addProseMirrorPlugins() {
return [
new Plugin({
appendTransaction: (_, oldState, newState) => {
let tr = newState.tr;
let oldParagraphCount = 0;
let newParagraphCount = 0;
const regex = /^(INT\.|EXT\.|int\.|ext\.|INT|EXT|int|ext|EST|EST\.|est|est\.)/;
oldState.doc.descendants((node) => {
if (node.type.name === 'paragraph') {
oldParagraphCount++;
}
});
newState.doc.descendants((node, pos) => {
if (node.type.name === 'paragraph' && regex.test(node.textContent)) {
if (!appliedPositions.has(pos)) {
let from = pos + 1;
let to = from + node.textContent.length;
let fragment = Fragment.from(newState.schema.text(sceneCount + "." + " " + node.textContent.toUpperCase() + "." + " "));
tr.replaceWith(from, to, fragment);
appliedPositions.add(pos);
sceneCount++;
}
}
if (node.type.name === 'paragraph') {
newParagraphCount++;
}
});
if (newParagraphCount > oldParagraphCount) {
paragraphCount++;
console.log(`Paragraph inserted. Total paragraphs: ${paragraphCount}`); // Print paragraph count
} else if (newParagraphCount < oldParagraphCount) {
paragraphCount = 0;
sceneCount = 1; // Resets scene count only when paragraphs are deleted
appliedPositions.clear();
console.log(`Paragraphs deleted. Paragraph and scene count reset to zero.`); // Print paragraph and scene count reset
}
return tr;
},
}),
];
},
});
export default SceneTitle;
Now I can't figure out how to make the scene title have progressive numbering, that it doesn't loop, and that all the text in that paragraph is uppercase.
The other problem I ran into is that simulating what potentially a user will be able to do, remember they are screenwriters, filmmakers and videomakers, is to go and insert a new scene title above the existing one only I was able to get it to update but not properly, that is, the new title had the sequence number following the last paragraph created, so:
- First example of incorrect numbering.:
"1. INT. ..."
"1. EXT. ..."
"2. INT. ..."
"3. EXT. ..."
- Second example of incorrect numbering.:
"4. INT. ..."
"1. EXT. ..."
"2. INT. ..."
"3. EXT. ..."
The technology stack I am using is as below:
- NextJS v14.1.0,
- TailwindCSS,
- TipTap v2,
- Typescript,
- NodeJS v20.8.0
Below are a couple of examples of what it should look like:
- First example of how the scene title should be numbered:
- INT. PUB - NIGHT
- Example of what the numbered sequence with various scene titles should look like:
- INT. PUB - NIGHT
We reveal that SHAUN is sitting with a woman, LIZ. They are
both in their late twenties. LIZ looks slightly concerned,
SHAUN looks slightly confused. They are having a drink.- INT. APARTMENT COMPLEX - MORNING
- INT. APARTMENT - MORNING
- INT. LIVING ROOM - MORNING
Solution
I've never done some NextJS before, but I'll just give you some hints with a minimal vanilla JS example that you then can implement yourself in your app.
Step 1: the regular expression
As I mentioned in my comments, your regular expression should be rewritten to handle all your inputs. I've done it by handling:
An optional number with or without a dot. This one may be wrong, as you mentioned, so we'll just drop it.
A type of place (
INT
,EXT
orEST
), with an optional dot, and case-insensitive, in order to match "INT", "Int" or "int", etc.The place name (ex: "Bedroom").
The hyphen separator, with mandatory spaces around.
When it is (ex: "morning").
Based on the regex I posted by comment, with the additional handling of an eventual undesired number at the beginning:
/^(\d+\.?\s*)?(?<place_type>int|ext|est)\.?\s*(?<place>.*?)\s+-\s+(?<when>.*?)\s*$/i
As you see, I use named groups because I found them easier than indexed groups, especially if you change the regex later. Group names don't change, but indexed groups do.
Test it here with the details: https://regex101.com/r/qDPvYK/1
Step 2: converting paragraphs to headings
As I also mentioned in my comment, it may be far simpler to not count the title yourself and just do it with CSS counters.
Titles should not stay paragraphs. Why? For semantic reasons. A search engine will also be able to understand the content in a far better way.
You said you didn't manage to convert a paragraph into a heading.
Yes, you are right. But with a bit of googling, you'll quickly
find that you just have to create a new <h2>
tag and put it
instead of the <p>
tag. This can be done in a lot of ways. The
simplest I found is to get the parent node and then use the
.replaceChild()
method.
I've added a button to the HTML demo so that you can see the content before and after running the conversion algorithm.
For the uppercase feature, it's up to you to decide. Either do
it at rendering with a CSS text-transform
rule or do it by
calling the .toUpperCase()
in the JS. I prefer doing it with CSS
because I think that something such as "Hard Rock Cafe"
should stay as it is, respecting it's own case, but can be
displayed "HARD ROCK CAFE" graphically. The advantage
is that you could change that CSS in the future without loosing
the original case.
const regexTitle = /^(\d+\.?\s*)?(?<place_type>int|ext|est)\.?\s*(?<place>.*?)\s+-\s+(?<when>.*?)\s*$/gmi;
document.addEventListener('DOMContentLoaded', function() {
function convertToTitles() {
// Loop over all paragraphs to try and convert some to headings.
document.querySelectorAll('p').forEach((p, i) => {
const match = regexTitle.exec(p.innerText);
if (match) {
const placeType = match.groups.place_type.toUpperCase(),
place = match.groups.place,
when = match.groups.when;
const h2 = document.createElement('h2');
h2.innerText = placeType + '. ' + place + ' - ' + when;
p.parentNode.replaceChild(h2, p);
}
});
}
const convertButton = document.getElementById('convert-to-titles');
convertButton.addEventListener('click', convertToTitles);
});
body {
background: silver;
padding: 2em;
}
.scenario {
min-width: 15em;
max-width: 30em;
background: white;
/* More padding on the left to pull out the scene title number. */
padding: 2em 2em 2em 4em;
margin-bottom: 2em;
box-shadow: 0 0 .5em rgba(0, 0, 0, 0.2);
font-family: "Courrier New", Courrier, monospace;
counter-reset: scene-title-nbr;
}
h1 {
margin-top: 0;
}
h2 {
margin: 1em 0 .5em 0;
font-size: 1em;
text-transform: uppercase;
position: relative; /* For absolute ::before pseudo-element. */
}
h2::before {
content: counter(scene-title-nbr);
counter-increment: scene-title-nbr;
position: absolute;
left: -2em;
}
<article class="scenario">
<header>
<h1>Scenario</h1>
</header>
<p>INT. Pub - night</p>
<p>
We reveal that SHAUN is sitting with a woman, LIZ. They are both in their
late twenties. LIZ looks slightly concerned, SHAUN looks slightly confused.
They are having a drink.
</p>
<p>INT. Apartment complex - morning</p>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quasi
maxime quisquam itaque numquam quas tempore quo, ex reprehenderit
ea dicta sequi voluptatem ipsum id cupiditate autem perspiciatis
obcaecati consectetur quod.
</p>
<p>1. INT. Apartment - morning</p>
<p>
Text should come here...
</p>
<p>INT. Living room - morning</p>
<p>
Text should come here...
</p>
<p>3. Ext Swimming-pool - midday</p>
<p>SHAUN dives into the pool while LIZ is sunbathing...</p>
</article>
<button id="convert-to-titles">Convert to titles</button>
The code can be improved when you create the title. Typically, it would be logical to have something like this, where the several fields of information in the title are split into sub tags:
.scenario {
font-family: "Courrier New", Courrier, monospace;
counter-reset: scene-title-nbr;
}
h2 {
font-size: 1em;
position: relative;
margin-left: 2em;
}
h2::before {
content: counter(scene-title-nbr);
counter-increment: scene-title-nbr;
position: absolute;
left: -2em;
}
.scene-title {
line-height: 1.4; /* Just to fix the clock emoji. */
}
.scene-title abbr.place-type {
text-decoration: none; /* override defaults */
}
.scene-title abbr.place-type:hover {
text-decoration: underline dotted silver; /* Show it on hover */
}
time[datetime="14:00"]::after {
content: " 🕑";
}
time[datetime="20:00"]::after {
content: " 🕗";
}
<article class="scenario">
<h2 class="scene-title">
<abbr class="place-type" title="Interior">INT</abbr>
<span class="place-name">Bedroom</span>
<span class="separator">-</span>
<time class="when" datetime="20:00">evening</time>
</h2>
<p>Some text...</p>
<h2 class="scene-title">
<abbr class="place-type" title="Exterior">EXT</abbr>
<span class="place-name">Swimming pool</span>
<span class="separator">-</span>
<time class="when" datetime="14:00">afternoon</time>
</h2>
<p>Some more text...</p>
</article>
Answered By - Patrick Janser
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.