From 38cd23c9d6a15576a09204ef5bc73031e64f179a Mon Sep 17 00:00:00 2001 From: HenryByte Date: Fri, 17 Jul 2026 23:09:04 +0100 Subject: [PATCH] Adding in ty and some ruff rules --- examples/Big-Fish.fountain | 420 ++++++------ pyproject.toml | 32 + .../export/courier_prime/LICENSE/OFL-FAQ.txt | 18 +- .../export/courier_prime/LICENSE/OFL.txt | 2 +- screenplain/export/courier_prime/Read me.txt | 4 +- screenplain/export/fdx.py | 48 +- screenplain/export/html.py | 79 +-- screenplain/export/pdf.py | 270 ++++---- screenplain/main.py | 140 ++-- screenplain/parsers/fountain.py | 118 ++-- screenplain/richstring.py | 134 ++-- screenplain/types.py | 15 +- setup.py | 53 +- tests/fdx_test.py | 29 +- tests/files/dialogue-with-blank-line.fountain | 2 +- tests/files/notes.fountain | 2 +- tests/files_test.py | 39 +- tests/fountain_test.py | 631 +++++++++--------- tests/richstring_test.py | 153 ++--- tests/visual/pdf_test.py | 9 +- uv.lock | 54 ++ 21 files changed, 1122 insertions(+), 1130 deletions(-) diff --git a/examples/Big-Fish.fountain b/examples/Big-Fish.fountain index a417999..3d85333 100644 --- a/examples/Big-Fish.fountain +++ b/examples/Big-Fish.fountain @@ -2,9 +2,9 @@ Title: Big Fish Credit: written by Author: John August Source: based on the novel by Daniel Wallace -Notes: +Notes: FINAL PRODUCTION DRAFT - includes post-production dialogue + includes post-production dialogue and omitted scenes Copyright: (c) 2003 Columbia Pictures @@ -16,12 +16,12 @@ This is a Southern story, full of lies and fabrications, but truer for their inc A RIVER. -We're underwater, watching a fat catfish swim along. +We're underwater, watching a fat catfish swim along. This is The Beast. EDWARD (V.O.) -There are some fish that cannot be caught. It's not that they're faster or stronger than other fish. They're just touched by something extra. Call it luck. Call it grace. One such fish was The Beast. +There are some fish that cannot be caught. It's not that they're faster or stronger than other fish. They're just touched by something extra. Call it luck. Call it grace. One such fish was The Beast. The Beast's journey takes it past a dangling fish hook, baited with worms. Past a tempting lure, sparkling in the sun. Past a swiping bear claw. The Beast isn't worried. @@ -33,16 +33,16 @@ INT. WILL'S BEDROOM - NIGHT (1973) WILL BLOOM, AGE 3, listens wide-eyed as his father EDWARD BLOOM, 40's and handsome, tells the story. In every gesture, Edward is bigger than life, describing each detail with absolute conviction. EDWARD -I didn't put any stock into such speculation or superstition. All I knew was I'd been trying to catch that fish since I was a boy no bigger than you. +I didn't put any stock into such speculation or superstition. All I knew was I'd been trying to catch that fish since I was a boy no bigger than you. (closer) And on the day you were born, that was the day I finally caught him. EXT. CAMPFIRE - NIGHT (1977) -A few years later, and Will sits with the other INDIAN GUIDES as Edward continues telling the story to the tribe. +A few years later, and Will sits with the other INDIAN GUIDES as Edward continues telling the story to the tribe. EDWARD -Now, I'd tried everything on it: worms, lures, peanut butter, peanut butter-and-cheese. But on that day I had a revelation: if that fish was the ghost of a thief, the usual bait wasn't going to work. I would have to use something he truly desired. +Now, I'd tried everything on it: worms, lures, peanut butter, peanut butter-and-cheese. But on that day I had a revelation: if that fish was the ghost of a thief, the usual bait wasn't going to work. I would have to use something he truly desired. Edward points to his wedding band, glinting in the firelight. @@ -89,7 +89,7 @@ I followed that fish up-river and down-river for three days and three nights, un Will regards his father with exasperated contempt. EDWARD -With these two hands, I reached in and snatched that fish out of the river. I looked him straight in the eye. And I made a remarkable discovery. +With these two hands, I reached in and snatched that fish out of the river. I looked him straight in the eye. And I made a remarkable discovery. INT. TINY PARIS RESTAURANT (LA RUE 14°) - NIGHT (1998) @@ -103,7 +103,7 @@ This fish, the Beast. The whole time we were calling it a him, when in fact it Over near the doorway, we spot Sandra, just returned from the restrooms. She looks gorgeous. She couldn't be any happier if this were her own wedding. EDWARD -Now, I was in a situation. I could gut that fish and get my ring back, but doing so I would be killing the smartest catfish in the Ashton River, soon to be mother of a hundred others. +Now, I was in a situation. I could gut that fish and get my ring back, but doing so I would be killing the smartest catfish in the Ashton River, soon to be mother of a hundred others. Will can't take any more. Josephine tries to hold him back, but he gets up and leaves. Edward doesn't even notice. @@ -125,16 +125,16 @@ Will can't articulate his anger. He just leaves. EDWARD Now, you may well ask, since this lady fish wasn't the ghost of a thief, why did it strike so quick on gold when nothing else would attract it? (closer; he holds up his ring) -That was the lesson I learned that day, the day my son was born. +That was the lesson I learned that day, the day my son was born. He focuses his words on Sandra. This story is -- and has always been -- about her more than anyone. EDWARD Sometimes, the only way to catch an uncatchable woman is to offer her a wedding ring. -A LAUGH from the crowd. +A LAUGH from the crowd. -Edward motions for Sandra to get up here with him. As she crosses, we can see that thirty years of marriage has not lessened their affection for each other. +Edward motions for Sandra to get up here with him. As she crosses, we can see that thirty years of marriage has not lessened their affection for each other. As they kiss, Edward tweaks her chin a special little way. The crowd APPLAUDS. @@ -151,7 +151,7 @@ What, a father's not allowed to talk about his son? WILL (disbelieving) -I am a footnote in that story. I am the context for your great adventure. Which never happened! Incidentally! You were selling novelty products in Wichita the day I was born. +I am a footnote in that story. I am the context for your great adventure. Which never happened! Incidentally! You were selling novelty products in Wichita the day I was born. EDWARD (shaking his head) @@ -179,16 +179,16 @@ Will won't let him get the last word. WILL You're embarrassing yourself, Dad. You just don't see it. -ANGLE ON Edward. Fine. A hand to wave, enough of you. +ANGLE ON Edward. Fine. A hand to wave, enough of you. He walks away. ANGLE ON Will, still fuming with righteous anger. It's then we FREEZE FRAME. WILL (V.O.)(CONT'D) -After that night, I didn't speak to my father again for three years. +After that night, I didn't speak to my father again for three years. -INT. A.P. NEWSROOM (PARIS) - DAY +INT. A.P. NEWSROOM (PARIS) - DAY A typically busy day. On hold with the phone cradled under an ear, Will sorts through a bundle of mail dropped on his desk. @@ -231,7 +231,7 @@ Edward stares intently into the water, a lion in wait. WILL (V.O.) In telling the story of my father's life, it's impossible to separate the fact from the fiction, the man from the myth. The best I can do is to tell it the way he told me. -We LOOK DOWN at the river, where Edward's reflection is caught in the dark water. As the water ripples past, something changes. +We LOOK DOWN at the river, where Edward's reflection is caught in the dark water. As the water ripples past, something changes. Sure enough, as we LOOK UP again, it's a younger EDWARD BLOOM, 20's, staring into the water. He's not just handsome, not just charming. It's as if all the forces of the natural world had conspired to create him. @@ -253,12 +253,12 @@ TITLE OVER: > _BIG FISH_ < -INT. HOSPITAL ROOM - DAY +INT. HOSPITAL ROOM - DAY Young Dr. Bennett stands between the Wife's legs. She's flustered and sweating, but the doctor has a comforting bedside manner... YOUNG DR. BENNETT -Now, Mrs. Bloom, I'll need you to give me one good push. On three. One... +Now, Mrs. Bloom, I'll need you to give me one good push. On three. One... Suddenly, we hear a POP as a slimy mass of human being rockets into the doctor's unprepared hands. Bennett tries to hold tight, but the infant is slippery like a fish. It shoots up into air. @@ -266,7 +266,7 @@ The NURSES and the Husband try to grab the baby, but no one can hold it. As the As it falls, the newborn knocks over a tray, which provides it a ramp to slide right out of the room. Everyone races after it. -INT. HOSPITAL HALLWAY - DAY +INT. HOSPITAL HALLWAY - DAY Bursting through the doors -- @@ -280,11 +280,11 @@ My father's birth would set the pace for his unlikely life. No longer than most INT. HALF-DARK PARIS APARTMENT - (PRESENT) DAY -Over the sound of rain, a phone RINGS on a chair. By the tone of the ring, we know we're not in the U.S. -- it has that insistent European sound. +Over the sound of rain, a phone RINGS on a chair. By the tone of the ring, we know we're not in the U.S. -- it has that insistent European sound. As it keeps RINGING, we look to see the apartment is mostly empty, just a few half-unpacked boxes. A cradle is still in its carton. -KEYS in the lock. LAUGHTER in the hallway. The door swings open to reveal a drenched Will (29) carrying four sacks of groceries, the bottoms collapsing from the rain. His wife Josephine (28) pushes past him to get the phone. +KEYS in the lock. LAUGHTER in the hallway. The door swings open to reveal a drenched Will (29) carrying four sacks of groceries, the bottoms collapsing from the rain. His wife Josephine (28) pushes past him to get the phone. JOSEPHINE Allo oui? @@ -339,15 +339,15 @@ I'm going with you. INT. AIR FRANCE 747 - NIGHT -As the plane continues boarding, a STEWARDESS recites the welcome spiel in French. Will has a window seat in coach. Josephine sits beside him, putting on hand lotion. +As the plane continues boarding, a STEWARDESS recites the welcome spiel in French. Will has a window seat in coach. Josephine sits beside him, putting on hand lotion. Taking his hands, she rubs the excess into him. There's an effortless intimacy between them. She can pinpoint what he's feeling before he can. INT. 747 / FLYING - NIGHT -Hours later, and the lights are dimmed. Most of the PASSENGERS are asleep, including Josephine. Her head is propped against Will's shoulder, her hands tucked under her belly. +Hours later, and the lights are dimmed. Most of the PASSENGERS are asleep, including Josephine. Her head is propped against Will's shoulder, her hands tucked under her belly. -Will watches her sleep, brushing back her hair. A beat, then he notices a BORED BOY in the next row over. +Will watches her sleep, brushing back her hair. A beat, then he notices a BORED BOY in the next row over. Off the glow of the reading light, the boy is using his hands to cast shadows on the seat back. The kid is pretty good, making a convincing bird, a passable monkey, and finally a dog. @@ -360,9 +360,9 @@ Focusing on the final shadow, we... MATCH CUT TO: -INT. BLOOM HOUSE - NIGHT +INT. BLOOM HOUSE - NIGHT -...come to find Edward making the shapes. +...come to find Edward making the shapes. Will (6) sits in his pajamas on the floor next to him. The endtable lamp lies between them, its shade off to cast big shadows on the wall. @@ -385,7 +385,7 @@ Will smiles, excited to hear the forbidden story. EDWARD (CONT'D) This all happened in the swamp outside of Ashton. Kids weren't supposed to go out in the swamp, on account of the snakes and spiders and quicksand that would swallow you up before you could even scream. But there were five of us out there that night: Me, Ruthie, Wilbur Freely, and the Price Brothers, Don and Zacky. -Edward holds up his hand, counting the names on his fingers. +Edward holds up his hand, counting the names on his fingers. EDWARD (CONT'D) Not a one of us knew what was in store. @@ -398,7 +398,7 @@ A flashlight SWEEPS past. We are... EXT. FIELD AT THE SWAMP EDGE - NIGHT -The night is WHIRRING and BREATHING, alive. The moon hangs low, casting long shadows. +The night is WHIRRING and BREATHING, alive. The moon hangs low, casting long shadows. Five kids walk past in silhouette. Four have flashlights on. The fifth keeps tripping, crashing into YOUNG EDWARD (10). @@ -443,7 +443,7 @@ ZACKY Damn. WILBUR FREELY -Screw. +Screw. EDWARD (whispering) @@ -451,12 +451,12 @@ Turn off your flashlights! She'll see 'em. MOVING UP behind the kids, we find ourselves at the gates of... -EXT. A CREEPY OLD HOUSE - NIGHT +EXT. A CREEPY OLD HOUSE - NIGHT ADULT EDWARD (V.O.) Now, it's common knowledge that most towns of a certain size have a witch, if only to eat misbehaving children and the occasional puppy who wanders into her yard. Witches use those bones to cast spells and curses that make the land infertile. -We PULL BACK, and BACK, revealing more of the Gothically creepy house: its broken windows, strangling vines, and eerie gargoyles half-buried in the dirt. Even bats are afraid to fly over it. +We PULL BACK, and BACK, revealing more of the Gothically creepy house: its broken windows, strangling vines, and eerie gargoyles half-buried in the dirt. Even bats are afraid to fly over it. In the moonlight, the house is especially sinister. Who knows what is lurking in the shadows? @@ -504,13 +504,13 @@ She'll make soap out of you! (to Ruthie) That's what she does, she makes soap out of people. -Edward drops down on the far side of the gate. Truth be told, Edward is scared, but he forges ahead anyway. +Edward drops down on the far side of the gate. Truth be told, Edward is scared, but he forges ahead anyway. Wilbur looks to Ruthie, and they're in complete agreement. They get the hell out of there. Zacky would run too, but Don holds him by the collar. EXT. APPROACHING THE HOUSE -Edward curves around the tall bushes that hide the front door. Anything could jump out of them. +Edward curves around the tall bushes that hide the front door. Anything could jump out of them. He steps on the porch. The boards SQUEAL and CREAK, but he continues on. A cat SCREAMS OUT from a broken wicker rocker. Catching his breath, Edward reaches the front door. @@ -524,7 +524,7 @@ EDWARD (calm and straightforward) Ma'am, my name is Edward Bloom, and there's some folks'd like to see your eye. -EXT. BACK AT THE GATE - NIGHT +EXT. BACK AT THE GATE - NIGHT Zacky and Don Price wait for Edward, each moment more convinced he's already dead. But suddenly, he's back at the gate. @@ -540,7 +540,7 @@ Let's see it. The Old Woman steps out of the shadows behind Edward, flipping up her eye patch. When their flashlight beam hits her left eye, it shines with a hellish glow. -We RUSH IN on Zacky, who is paralyzed by what he sees. +We RUSH IN on Zacky, who is paralyzed by what he sees. CUT TO: @@ -548,7 +548,7 @@ EXT. FRONT PORCH OF HOUSE - DAY An OLD MAN -- Zacky -- stands on a wobbly stepladder, changing a lightbulb. Suddenly, the ladder gives way and he falls. Dead. -EXT. AT THE GATE - NIGHT +EXT. AT THE GATE - NIGHT We RUSH IN on Don Price. @@ -558,7 +558,7 @@ INT. FRATERNITY HOUSE BATHROOM - DAY Twenty-year old Don Price falls face-forward on the tile, face mushed in the grout. Very much dead. -EXT. AT THE GATE - NIGHT +EXT. AT THE GATE - NIGHT Don and Zacky both tremble with fear. The latter has tears in his eyes. @@ -605,7 +605,7 @@ MATCH CUT TO: INT./EXT. BLOOM HOUSE - (PRESENT) DAY -The front door opens to reveal Will and Josephine on the porch with their bags. REVERSE to Will's mother Sandra (53), surprised and a little annoyed. +The front door opens to reveal Will and Josephine on the porch with their bags. REVERSE to Will's mother Sandra (53), surprised and a little annoyed. SANDRA How did you get here? @@ -638,7 +638,7 @@ That hug finished, Sandra pushes past her son to her daughter-in-law. Seeing th SANDRA You shouldn't have flown. But... -They hug. +They hug. JOSEPHINE It's good to see you. You look beautiful. @@ -761,7 +761,7 @@ Edward cracks open an eye, a beat before he focuses. He tries to say something, He looks over at a pitcher on the nightstand. Will pours him a glass of water, helping him hold it to his parched lips. -Finished, Edward sets down the glass by himself. A very long, tense beat. Will almost speaks again to fill the silence. +Finished, Edward sets down the glass by himself. A very long, tense beat. Will almost speaks again to fill the silence. Finally... @@ -923,7 +923,7 @@ I wish. CUT TO: -INT. TINY CHURCH - DAY +INT. TINY CHURCH - DAY Wearing a white shirt and tie, YOUNG EDWARD -- still about 10 -- sings "Down to the River My Lord" along with the CONGREGATION. His voice is high and thin, but he gives it his all. @@ -938,7 +938,7 @@ As we watch, Edward's pant cuffs rise inch by inch -- that's how fast he's growi EDWARD (V.O.) Truth is, no one quite knew what was wrong. Most times, a person grows up gradually. I found myself in a hurry. -INT. YOUNG EDWARD'S BEDROOM - DAY +INT. YOUNG EDWARD'S BEDROOM - DAY Young Edward lies in bed, his limbs connected to various pulleys and levers to support his weight. He has a dozen encyclopedias around him, and another dozen on the floor. @@ -956,16 +956,16 @@ Young Edward thinks this through. EDWARD (V.O.) It occurred to me then, that perhaps the reason for my growth was that I was intended for larger things. After all, a giant man can't have an ordinary-sized life. -EXT. BASEBALL FIELD - DAY +EXT. BASEBALL FIELD - DAY The CRACK of a bat announces the game-winning home run. The crowd CHEERS the swing, and especially the batter as he rounds the bases. -Although we've seen him briefly before, this is our first real exposure to GROWN-UP EDWARD, who we'll follow from roughly the ages of 18 to 30. +Although we've seen him briefly before, this is our first real exposure to GROWN-UP EDWARD, who we'll follow from roughly the ages of 18 to 30. EDWARD (V.0.) As soon as my bones had settled in their adult configuration, I set upon my plan to make a bigger place for myself in Ashton. -EXT. SCHOOL FIELDS - DAY +EXT. SCHOOL FIELDS - DAY SINGLE SHOTS: Football hero Edward leads his team to victory. On the sidelines, a PRETTY GIRL admits the name of her secret love: @@ -974,21 +974,21 @@ Edward Bloom! The other GIRLS SQUEAL in agreement. Don Price looks over, glowers. -EXT. NEIGHBORHOOD - DAY +EXT. NEIGHBORHOOD - DAY SINGLE SHOT: A lawnmower ROARS along the grass. We LOOK UP to see who's pushing it, but it's not Edward. It's one of his teenage EMPLOYEES. -Edward is back at the truck, which is painted to read, "Bloom Landscaping." He has workers on every lawn. +Edward is back at the truck, which is painted to read, "Bloom Landscaping." He has workers on every lawn. He signs an autograph for an ADMIRING CUB SCOUT. -INT. BASKETBALL COURT - DAY +INT. BASKETBALL COURT - DAY Edward takes an impossible shot at the buzzer from the other end of the court. Naturally, he makes it, winning the game. As the crowd goes wild for Edward, Don Price is the only teammate who doesn't mob him. -EXT. TOWN - DAY +EXT. TOWN - DAY Edward carries a dog out of a burning house. @@ -1004,7 +1004,7 @@ A dashingly handsome Edward leads the CAST out for a curtain call. He's the sta Edward soaks in his applause, smiling and gracious. -EXT. GRADUATION STAGE - DAY +EXT. GRADUATION STAGE - DAY Edward accepts his diploma. The PRINCIPAL hugs him tight. @@ -1017,7 +1017,7 @@ As two FARMERS shake their heads, we REVERSE to a show a massive hole punched th EXT. SHEEP PEN - DAY -Two fat ewes look up, a shadow falling across them. They BLEAT in panic as +Two fat ewes look up, a shadow falling across them. They BLEAT in panic as TWO OVERSIZED HANDS @@ -1083,11 +1083,11 @@ GO AWAY! The giant's voice has such force, it blows Edward's hair back. EDWARD -I'm not going anywhere until you show yourself. +I'm not going anywhere until you show yourself. A beat, then we hear a RUMBLE, like a train coming. Edward braces himself, fists ready for a fight, if that's what it's going to take. -As the RUMBLE gets louder, the ground starts to shake. Even Edward starts to worry. Just how big is this guy? +As the RUMBLE gets louder, the ground starts to shake. Even Edward starts to worry. Just how big is this guy? EDWARD (V.O.)(CONT'D) Armed with the foreknowledge of my own death, I knew the giant couldn't kill me. All the same, I preferred to keep my bones unbroken. @@ -1106,7 +1106,7 @@ Why are you here? Edward ponders the best response, settling on... EDWARD -So you can eat me. The town decided to send a human sacrifice, and I volunteered. +So you can eat me. The town decided to send a human sacrifice, and I volunteered. Karl's eyes narrow, confused. Edward stands up. @@ -1168,9 +1168,9 @@ KARL Karl. EDWARD -Mine's Edward. And truthfully, I do want you to leave, Karl. But I want to leave with you. +Mine's Edward. And truthfully, I do want you to leave, Karl. But I want to leave with you. (closer) -You think this town is too small for you, well, it's too small for a man of my ambition. I can't see staying here a day longer. +You think this town is too small for you, well, it's too small for a man of my ambition. I can't see staying here a day longer. KARL You don't like it? @@ -1209,7 +1209,7 @@ Edward ducks a bit so the Mayor can put the key around his neck. The crowd CHEE Only DON PRICE, smoking on the corner, isn't sad to see Edward go. He crushes his cigarette under his heel. He wishes he could crush Edward. -Many of the townfolk come onto the street to hug Edward or shake his hand. +Many of the townfolk come onto the street to hug Edward or shake his hand. EDWARD (V.O.) That afternoon as I left Ashton, everyone seemed to have advice. @@ -1218,11 +1218,11 @@ VARIOUS TOWNFOLK Find yourself a nice girl! Don't trust anyone in Kentucky! Watch your pride, Edward Bloom! EDWARD (V.O.) -But there was one person whose counsel I held above all others. +But there was one person whose counsel I held above all others. -As the crowd parts, he finds himself face to face with +As the crowd parts, he finds himself face to face with -THE OLD WOMAN. +THE OLD WOMAN. The ruckus slows and quiets, as if a strange spell has been cast. She motions for Edward to lean down, so she can whisper something to him. Although we're VERY CLOSE, we can't hear her voice. @@ -1271,15 +1271,15 @@ KARL You're not trying to run away? EDWARD -Just to be sure, you can take my pack. +Just to be sure, you can take my pack. Karl perks up, even though it means more for him to carry. -EXT. DIRT ROAD - DAY +EXT. DIRT ROAD - DAY The road is overgrown, but not altogether creepy. The sun is still shining, and the birds still CHIRPING. -Spinning the Key to the City, Edward WHISTLES, because it's a day meant for whistling. +Spinning the Key to the City, Edward WHISTLES, because it's a day meant for whistling. EXT. FURTHER ALONG - ROUGH PATH @@ -1291,7 +1291,7 @@ He looks back the way he came. It's tempting to go back. It would be easier to He carefully steps through the thorns. His trouser legs catch on the barbs. We can hear the fabric TEAR. -FURTHER ALONG +FURTHER ALONG A scratched and sweaty Edward waves off various STINGING BUGS flying at him, finally whipping off his hat to swat at them. Just then a CAWING crow swoops down and grabs the hat right out of his hands. @@ -1304,29 +1304,29 @@ Edward high-tails it, each step still precarious. EXT. THE DARK FOREST - DAY [LATER] -Edward is bruised, battered and bee-stung. +Edward is bruised, battered and bee-stung. A half-broken sign lies in the road. Edward picks it up. Reads it: -WARNING! +WARNING! JUMPING SPIDERS! Sure enough, up ahead he sees the path is overgrown with thick cobwebs, heavy from the rain. EDWARD (V.O.) -There comes a point where a reasonable man will swallow his pride and admit he's made a terrible mistake. The truth is, I was never a reasonable man. +There comes a point where a reasonable man will swallow his pride and admit he's made a terrible mistake. The truth is, I was never a reasonable man. Edward tosses the sign and forges ahead, into the spiderwebs. EDWARD (CONT'D) And what I recalled of Sunday School was that the more difficult something became, the more rewarding it was in the end. -EXT. CLEARING / THE ROAD - DAY +EXT. CLEARING / THE ROAD - DAY Edward emerges from the forest, brushing the last cobwebs off and shaking the spiders from his shirt. One is stuck in his sleeve, and he has to dance to get it out. Even then, he still keeps twitching, convinced another one is left behind. -At his feet, the gravel road has returned, smooth and dusty and comforting. +At his feet, the gravel road has returned, smooth and dusty and comforting. Ahead lies a tiny one-street town -- smaller even than Ashton -- with powerlines emerging from the woods to feed it. Dangling from the line above he sees two dozen pairs of shoes, their laces tied together. @@ -1431,7 +1431,7 @@ Hey! He chases after her. -EXT. TOWN / MAIN STREET - DAY +EXT. TOWN / MAIN STREET - DAY As she runs, Jenny ties Edward's laces together. Reaching the edge of town, she tosses the shoes up and around the power line -- a perfect throw. There's no way he's ever getting them down. @@ -1454,9 +1454,9 @@ He is our poet laureate. The townsfolk continue to congratulate Edward... EDWARD (V.O.) -Sometimes in a dream, you'll visit places that seem instantly familiar, filled with friends you've never met. +Sometimes in a dream, you'll visit places that seem instantly familiar, filled with friends you've never met. -EXT. UNDER A TREE - DUSK +EXT. UNDER A TREE - DUSK Edward sits with Norther Winslow. The fireflies are out. Thousands of them. @@ -1497,7 +1497,7 @@ Norther doesn't answer. Just goes back to his notebook. EXT. BY THE RIVER - NIGHT -By the light of the full moon, Edward soaks his feet in the water, trying to make sense of it all. The Key to the City dangles around his neck. +By the light of the full moon, Edward soaks his feet in the water, trying to make sense of it all. The Key to the City dangles around his neck. He stares at himself in the reflection. He smiles. @@ -1507,13 +1507,13 @@ She stands in the river with her bare back to Edward, squeezing the water out of Then he sees the snake. -It's a cottonmouth, has to be. It leaves a break in the water, its small reptilian head aiming for her flesh. There's no decision to be made. On pure instinct, Edward dives in. He swims as hard as can, +It's a cottonmouth, has to be. It leaves a break in the water, its small reptilian head aiming for her flesh. There's no decision to be made. On pure instinct, Edward dives in. He swims as hard as can, GRABBING THE SNAKE just as it's about to strike. -The woman dives back underwater, understandably terrified that a man is coming at her. +The woman dives back underwater, understandably terrified that a man is coming at her. EDWARD No, it's okay! I got it. I got the snake. @@ -1578,7 +1578,7 @@ EDWARD Eighteen. JENNY -I'm eight. That means when I'm eighteen, you'll be 28. And when I'm 28, you'll only be 38. +I'm eight. That means when I'm eighteen, you'll be 28. And when I'm 28, you'll only be 38. EDWARD (a little wary) @@ -1626,7 +1626,7 @@ EDWARD This town is everything a man could ask for. And if I were to end up here, I'd consider myself lucky. But the fact is, I'm not ready to end up anywhere. BEAMEN -No one's ever left. +No one's ever left. JENNY How are you gonna make it without your shoes? @@ -1634,7 +1634,7 @@ How are you gonna make it without your shoes? EDWARD I suspect it will hurt a lot. -And with that, Edward walks down Main Street. The townspeople stop dancing, disbelieving, some shaking their heads. +And with that, Edward walks down Main Street. The townspeople stop dancing, disbelieving, some shaking their heads. Poor Edward Bloom's gone crazy. @@ -1670,7 +1670,7 @@ As difficult as it was to reach Spectre, I was fated to get there eventually. A As he ducks under branches, the chain holding the Key to the City gets caught. He's almost strangled, but the chain finally breaks. The silver key disappears into the mud. -Scrambling forward, he looks for a way out. But the trees have encircled him, their spiky crowns bending down to crush him. +Scrambling forward, he looks for a way out. But the trees have encircled him, their spiky crowns bending down to crush him. He SCREAMS up at the night, until his breath is gone. @@ -1692,9 +1692,9 @@ His bare foot steps onto asphalt. A DEEP VOICE Friend! -Edward turns to see +Edward turns to see -KARL +KARL to his right, coming down the larger, paved road. @@ -1706,11 +1706,11 @@ Edward looks down at his muddy, bloody feet. EDWARD They got ahead of me. -With that, the men start walking down the larger road. +With that, the men start walking down the larger road. CROSSFADE TO: -INT. DINING ROOM - NIGHT +INT. DINING ROOM - NIGHT Edward and Will sit at opposite ends of the table, with Sandra and Josephine in the middle. Although Edward has a small plate of food in front of him, he hasn't touched it. He's exhausted from the trip downstairs, but determined to maintain the family dinner ritual. @@ -1795,7 +1795,7 @@ WILL Yes. But that's a different... SANDRA -No it's not. It's exactly the same. Your father and I met, we dated, and we married -- we chose each other -- because we understood each other on some fundamental level. Just the same as you two. +No it's not. It's exactly the same. Your father and I met, we dated, and we married -- we chose each other -- because we understood each other on some fundamental level. Just the same as you two. She moves on to the carrots. @@ -1827,7 +1827,7 @@ Reaching the CASHIER, Sandra hands over her coupons. Will is approaching with a Two checkstands over, an ATTRACTIVE BLONDE WOMAN in her 50's is getting her change. Though she's Sandra's generation, she carries herself like a much younger woman, with blue jeans and sneakers. -She accidentally makes eye contact with Will as he passes. We HOLD ON the woman, who tracks Will as he reaches Sandra. It's hard to read her reaction: does she recognize him, or just find him attractive? +She accidentally makes eye contact with Will as he passes. We HOLD ON the woman, who tracks Will as he reaches Sandra. It's hard to read her reaction: does she recognize him, or just find him attractive? Will notices the gaze. The woman turns away. @@ -1860,7 +1860,7 @@ Never seen her before. Pretty, though. INT. GUEST BEDROOM - NIGHT -A portable fan quietly WHIRRS in the corner. Turned low, the RADIO on the nightstand is playing a call-in AM sports show, just a wash of background chatter. Edward lies asleep on his back. +A portable fan quietly WHIRRS in the corner. Turned low, the RADIO on the nightstand is playing a call-in AM sports show, just a wash of background chatter. Edward lies asleep on his back. At the window, Josephine quietly lowers the shade. She reaches over Edward to switch off the radio. He stirs from the silence -- he wasn't fully asleep -- and sees Josephine stretched over him. @@ -1887,7 +1887,7 @@ I don't usually remember unless they're especially portentous. You know what th She shakes her head. EDWARD (CONT'D) -Means when you dream about something that's going to happen. +Means when you dream about something that's going to happen. (beat, gathering) Like one night, I had a dream where this crow came and told me, "Your Aunt is going to die." I was so scared I woke up my parents. They told me it was just a dream, to go back to bed. But the next morning, my Aunt Stacy was dead. @@ -1895,18 +1895,18 @@ JOSEPHINE That's terrible. EDWARD -Terrible for her, but think about me, young boy with that kind of power. Wasn't three weeks later that the crow came back to me in a dream and said, "Your Grampa is going to die." Well, I ran right back to my parents. My father said, no, Gramps is fine, but I could see there was trepidation. And true enough, that next morning my Grampa was dead. +Terrible for her, but think about me, young boy with that kind of power. Wasn't three weeks later that the crow came back to me in a dream and said, "Your Grampa is going to die." Well, I ran right back to my parents. My father said, no, Gramps is fine, but I could see there was trepidation. And true enough, that next morning my Grampa was dead. He sits up a bit in bed, his strength returning. EDWARD (CONT'D) -For the next couple weeks, I didn't have another dream. Until one night the crow came back and said, "Your Daddy is going to die." +For the next couple weeks, I didn't have another dream. Until one night the crow came back and said, "Your Daddy is going to die." (beat) Well, I didn't know what to do. But finally I told my father. And he said not to worry, but I could tell he was rattled. That next day, he wasn't himself, always looking around, waiting for something to drop on his head. Because the crow didn't tell how it was going to happen, just those words: your Daddy is going to die. Well, he went into town early and was gone for a long time. And when he finally came back, he looked terrible, like he was waiting for the axe to fall all day. He said to my mother, "Good God. I just had the worst day of my life." (beat) "You think _you've_ had a bad day," she said. "This morning the milkman dropped dead on the porch!" -Josephine smiles, a half-laugh, which gets him smiling too. +Josephine smiles, a half-laugh, which gets him smiling too. A long beat. Then, deadpan... @@ -1922,7 +1922,7 @@ He was slipping her a little extra cream. She nods, a bit more of a laugh. EDWARD (CONT'D) -He was filling her basket. He was making deliveries around back. +He was filling her basket. He was making deliveries around back. As Edward continues, she can't help but laugh harder, especially as the metaphors get more vulgar. @@ -1935,7 +1935,7 @@ Stop. EDWARD They were squeezing the cheese. Clanking the bottles. Licking the popsicle. -She's starting to cry from laughing. +She's starting to cry from laughing. EDWARD (CONT'D) Cracking the eggs and making an omelet. @@ -1947,7 +1947,7 @@ Spooning the sherbet. JOSEPHINE (interrupting) -Can I take your picture? +Can I take your picture? EDWARD You don't need a picture. Just look up handsome in the dictionary. @@ -2002,13 +2002,13 @@ EXT. OLYMPIA CIRCUS - NIGHT ...where the second-rate carnival is parked for the moment in an Alabama field. To the left, we spot Edward, 20-ish, halfway through a bag of peanuts. He's still carrying the backpack we saw earlier, and scratched up from his trip through Spectre. EDWARD (V.O.) -I had just left Ashton, and was on my way to discover my destiny. Not knowing what that would be exactly, I explored every opportunity that presented itself. +I had just left Ashton, and was on my way to discover my destiny. Not knowing what that would be exactly, I explored every opportunity that presented itself. Joining the crowd, he heads into the big-top. INT. BIG TOP - NIGHT -A troupe of STILT-WALKING FIREBREATHERS finishes their act to tremendous APPLAUSE. +A troupe of STILT-WALKING FIREBREATHERS finishes their act to tremendous APPLAUSE. As the performers clear away, the circus' owner-and-ringmaster AMOS CALLOWAY (50) approaches the stands. He may only be four feet tall, but Amos has a titanic presence. @@ -2035,11 +2035,11 @@ A foot suddenly bursts out from within. GASPS from the crowd. That foot is mas As the drum beat intensifies, a second foot breaks out. Followed by hands. Shoulders. Finally, the head. This is COLOSSUS. -From a very LOW ANGLE, we look up to see just how massive he is. He seems to fill the Heavens. With his shaved head and giant club, he seems more ogre than man. +From a very LOW ANGLE, we look up to see just how massive he is. He seems to fill the Heavens. With his shaved head and giant club, he seems more ogre than man. -In the bandstands, a YOUNG BOY's jaw drops in awe. Colossus walks down the row, letting the crowd get a better look at him. Some reach out to touch him, disbelieving. A tight spotlight follows him, revealing faces in the crowd. +In the bandstands, a YOUNG BOY's jaw drops in awe. Colossus walks down the row, letting the crowd get a better look at him. Some reach out to touch him, disbelieving. A tight spotlight follows him, revealing faces in the crowd. -Colossus passes Edward, who seems unimpressed. He leans with the spotlight, WHISTLING to get the big man's attention. +Colossus passes Edward, who seems unimpressed. He leans with the spotlight, WHISTLING to get the big man's attention. He points to the edge of the stands, where his friend is sitting on the dirt -- @@ -2081,15 +2081,15 @@ It was on that night Karl met his destiny. And I met mine. Almost. INT. BIG TOP - NIGHT - CONTINUOUS -As Amos pulls Karl aside to give him the hard sell, Edward notices a BEAUTIFUL YOUNG WOMAN (16) leaving with her family. She's wearing a blue dress and hat. For no good reason, she looks back at Edward. +As Amos pulls Karl aside to give him the hard sell, Edward notices a BEAUTIFUL YOUNG WOMAN (16) leaving with her family. She's wearing a blue dress and hat. For no good reason, she looks back at Edward. -The two make eye contact. And as they do, all motion FREEZES. +The two make eye contact. And as they do, all motion FREEZES. A fiery baton remains mid-twirl, flames locked in place. A spilled box of popcorn hangs in mid-air, each kernel like a snowflake. Even the elephant is mid-poop. Only Edward is free to move, winding his way between the frozen bodies, ducking underneath arms to get closer and closer to this woman. -EDWARD (V.O.)(CONT'D) +EDWARD (V.O.)(CONT'D) They say when you meet the love of your life, time stops. And that's true. What they don't tell you, is that once time starts again, it moves extra fast to catch up. Suddenly, everything RUSHES. The crowd becomes a blur, and the young woman is lost in its wake. Now it's Edward who's frozen, helpless in time. @@ -2187,7 +2187,7 @@ Kid, you were a big fish in a small pond. This here is the ocean, and you're dr Getting in front of Amos, Edward stops him. EDWARD -Wait. You said I don't have a plan. I do. I'm going to find that girl and marry her and spend the rest of my life with her. +Wait. You said I don't have a plan. I do. I'm going to find that girl and marry her and spend the rest of my life with her. Amos smiles, amused. @@ -2225,7 +2225,7 @@ The only thing that kept me going was the promise of meeting the girl who would Nodding off, Edward falls backward, into the path of the spinning arms. One of the Hydra cars hits him square in the gut, throwing him up and away, sailing 200 feet through the air. -EXT. FIELD - DAY +EXT. FIELD - DAY Edward chases a costumed pig, tripping over tent cords, falling in the mud. @@ -2250,7 +2250,7 @@ This girl, the love of your life. Her favorite flower is daffodils. He walks away. We PUSH IN on Edward, enraptured by the concept. EDWARD -Daffodils. +Daffodils. (to Karl) Daffodils! @@ -2267,7 +2267,7 @@ The wonder of it. He goes back to shoveling, a smile on his face. EDWARD (V.O.)(CONT'D) True to his word, every month Amos would tell me something new about the woman of my dreams. -INT. A DARK PLACE - NIGHT +INT. A DARK PLACE - NIGHT CLOSE ON Edward, lost in quiet reverie, pondering his latest bit of information. @@ -2282,7 +2282,7 @@ INT. BIG TOP - NIGHT [CONTINUOUS] INT. STABLES - NIGHT -Under a full moon, Edward feeds the animals. +Under a full moon, Edward feeds the animals. EDWARD (to himself) @@ -2310,7 +2310,7 @@ Edward wrestles with the beast, its mouth snapping at his throat. Blocking with Entwined, they roll across the dirt. The other nearby CARNIES scatter for cover. Mr. Soggybottom pulls a revolver out of his clown suit. Loads a silver bullet. -Edward finally succeeds in throwing the beast off. He rolls to his feet. +Edward finally succeeds in throwing the beast off. He rolls to his feet. The hell hound squares back on its haunches, GROWLING, ready for another leap. Mr. Soggybottom sheds a clown tear, aiming the revolver at the dog. @@ -2327,9 +2327,9 @@ Licking its chops, the dog approaches the helpless Edward, who feels the ground Like magic, the dog's whole demeanor changes. It bounces excitedly, ready to play fetch. -Seeing an opportunity, Edward throws the stick as far as he can. The dog bounds after it, +Seeing an opportunity, Edward throws the stick as far as he can. The dog bounds after it, -SMASHING DOWN THREE CARS. +SMASHING DOWN THREE CARS. It returns a beat later with the flaming stick, which it drops at Edward's feet. Its tail whips back and forth. @@ -2382,7 +2382,7 @@ Edward walks away. Then starts running. He has to get there as soon as possibl Amos sits down and scratches his ear with his foot. -EXT. BIG TOP - DAY +EXT. BIG TOP - DAY Edward shakes Karl's giant hand. They hug. @@ -2393,11 +2393,11 @@ EXT. AUBURN UNIVERSITY - DAY We DESCEND ON the main quad, to find Edward Bloom dunking his head in the fountain. -He changes out of his grubby shirt into a new one, just out of the package. It's the mid-1960's, but by the conservative dress of the passing STUDENTS, it could be any era. +He changes out of his grubby shirt into a new one, just out of the package. It's the mid-1960's, but by the conservative dress of the passing STUDENTS, it could be any era. EXT. SORORITY HOUSE - DAY -Edward stands with a bouquet of daffodils in front of a half-open door. Through the crack we can see the edge of a YOUNG WOMAN, talking in hushed tones with another girl we can't see. +Edward stands with a bouquet of daffodils in front of a half-open door. Through the crack we can see the edge of a YOUNG WOMAN, talking in hushed tones with another girl we can't see. Finally, a decision is reached. The door opens to reveal the woman of Edward's dreams, Sandra Kay Templeton. She's effortlessly beautiful, pure and simple as sunlight. @@ -2429,7 +2429,7 @@ But you're wrong. I _do_ know you, at least by reputation. Edward Bloom from A FLASHCUTS TO: -EXT. CREEPY OLD HOUSE - THE GATE - NIGHT +EXT. CREEPY OLD HOUSE - THE GATE - NIGHT Young Don Price shines his flashlight on Edward. @@ -2450,14 +2450,14 @@ Edward is dumbstruck. With all the strength he can muster... EDWARD Well. Congratulations. I'm sorry to have bothered you. -He turns and walks down the front steps. +He turns and walks down the front steps. She stays in the doorway for a few beats, feeling genuinely horrible for what's happened. But eventually she goes back inside. We hear GIGGLES from inside as her sorority sisters get to the bottom of this. SANDRA Stop it. It's not funny. That poor boy. -We LEAD Edward as he walks away, tears just starting to form. +We LEAD Edward as he walks away, tears just starting to form. EDWARD (V.O.) Fate has a cruel way of circling around on you. After all this work to leave Ashton, the girl I loved was now engaged to one of its biggest jerks. @@ -2495,7 +2495,7 @@ Walking with her books, Sandra shakes her head, disbelieving. We look up to the INT. SANDRA'S BEDROOM - [THE NEXT] MORNING -At her Sisters' prompting, a just-woken Sandra looks out the second-story window to find the lawn filled with +At her Sisters' prompting, a just-woken Sandra looks out the second-story window to find the lawn filled with TEN THOUSAND DAFFODILS. @@ -2588,7 +2588,7 @@ MATCH CUT TO: EXT. WITCH'S HOUSE / GATE - NIGHT [FLASHBACK] -The same image of Don's dead face on the tile is reflected in The Eye. +The same image of Don's dead face on the tile is reflected in The Eye. RETURNING BACK TO: @@ -2599,7 +2599,7 @@ The thrashing continues. Edward somehow fights his way back to his feet, ready SANDRA Don! -Don is about to slug Edward again when he turns. +Don is about to slug Edward again when he turns. Sandra pulls off her engagement ring. There's an audible AHH! from her sisters, and an OHH! from Don's brothers. @@ -2616,7 +2616,7 @@ What. You love this guy? SANDRA He's almost a stranger and I prefer him to you. -She hands him the ring. Another beat, then Don storms off. But not before decking Edward one last time. +She hands him the ring. Another beat, then Don storms off. But not before decking Edward one last time. Sandra leans over Edward's broken body. His head lies on the daffodils. @@ -2680,12 +2680,12 @@ JOSEPHINE I like your stories. EDWARD -And I like you. +And I like you. He doesn't let the moment linger with undue sentimentality. There's a story to be told. EDWARD (CONT'D) -Now. The thing about working for a circus is you don't have a regular address, and after three years I had a lot of undelivered mail. +Now. The thing about working for a circus is you don't have a regular address, and after three years I had a lot of undelivered mail. In the hallway, Will shifts to a new position, letting himself listen to the story one more time. @@ -2723,7 +2723,7 @@ Digging a knife out of his pocket, Edward gets to work cutting through the cable EXT. OUTSIDE STAGE - NIGHT -A THOUSAND CHINESE SOLDIERS sit, bored, watching the equivalent of a U.S.O. show. +A THOUSAND CHINESE SOLDIERS sit, bored, watching the equivalent of a U.S.O. show. A CHINESE VENTRILOQUIST is on stage with his Communist puppet. We have no idea what they're saying to each other, but every act is fundamentally the same. @@ -2731,7 +2731,7 @@ The EMCEE comes on to usher him off the stage before he's finished. The Ventril EXT. HIGH ABOVE THE STAGE - NIGHT -We LOOK DOWN with Edward, who is drifting right for the stage. He can't steer. He's helpless. +We LOOK DOWN with Edward, who is drifting right for the stage. He can't steer. He's helpless. But then, a BLAST of fireworks from the sides of the stage. The lights go out as a DRUM ROLL begins. It's just enough cover for Edward to remain unseen. @@ -2743,7 +2743,7 @@ The curtain rises to reveal PING (27) at a microphone. She's as gorgeous a woma She stands with her hips turned in profile. Her body is a knockout, dress cut to reveal skin. The soldiers are on their feet, WHISTLING and HOLLERING. -UP ON THE CATWALK, Edward is surprised by an ENEMY SOLDIER. The two men begin to SCUFFLE. +UP ON THE CATWALK, Edward is surprised by an ENEMY SOLDIER. The two men begin to SCUFFLE. MUSIC starts, a vampy torch song. Ping sings melody while an off-stage voice carries perfect harmony. @@ -2755,7 +2755,7 @@ Wants to grab the first man she sees and tear off his clothes.* A ROAR from the soldiers. She knows what they want. -Still fighting, Edward jumps for a pole on the far side of the catwalk, sliding down it to end up +Still fighting, Edward jumps for a pole on the far side of the catwalk, sliding down it to end up BACKSTAGE. @@ -2785,7 +2785,7 @@ Two times the bad times in half the years. I need a strong man, because I've got Twice the love to give.* -For the first time, Ping turns, and now we see why she was standing in profile. Ping is one-half of +For the first time, Ping turns, and now we see why she was standing in profile. Ping is one-half of SIAMESE TWINS. @@ -2807,7 +2807,7 @@ I'm the bad one. As the MUSIC builds towards the climax, Edward finishes the fight, knocking the guard out with a right hook. -As Ping and Jing reach the last chorus, they strike a final pose in the shape of a heart, their arms forming the arches, their backs forming the curves. +As Ping and Jing reach the last chorus, they strike a final pose in the shape of a heart, their arms forming the arches, their backs forming the curves. Edward slips behind curtains, trying to get away. @@ -2819,7 +2819,7 @@ The APPLAUSE dies, replaced by a concerned RUMBLE. The Emcee yells for the Guar ON STAGE -Ping has no idea what's happening. Jing reaches into her cleavage to pull out her eyeglasses. +Ping has no idea what's happening. Jing reaches into her cleavage to pull out her eyeglasses. INT. DRESSING ROOM - NIGHT @@ -2845,24 +2845,24 @@ EDWARD I'm not going to hurt you. PING -Damn right you're not. +Damn right you're not. (yelling) GUARD! Jing grabs her, a hand over her mouth, but it's too late. A rifle-toting GUARD looks in. -Pretending to be her bitchy sister -- +Pretending to be her bitchy sister -- JING Tell your men not to bother us! And lock that door! -The guard obeys. Ping shakes her sister off. Desperately flipping through his Asian phrasebook, he finds... +The guard obeys. Ping shakes her sister off. Desperately flipping through his Asian phrasebook, he finds... EDWARD Please, I need your help. PING -What makes you think we'll help you? +What makes you think we'll help you? Edward pulls a photo out of his flak jacket. It's Sandra. @@ -2899,7 +2899,7 @@ EXT. TEMPLETON FAMILY HOUSE - DAY Sandra checks the mail, hoping for a letter from Edward. EDWARD (V.O.) -And so the twins and I began our arduous journey halfway around the world. Unfortunately, there was no way to send a message back to America. +And so the twins and I began our arduous journey halfway around the world. Unfortunately, there was no way to send a message back to America. A black car pulls up. Two ARMY OFFICERS get out. @@ -2915,9 +2915,9 @@ Sandra hangs sheets to dry on the clotheslines, forming a tunnel of fabric. EDWARD (V.O.) After four months, Sandra had gotten over the worst of the nightmares. When the phone rang, she didn't think it was somehow me calling her. When a car drove past, she didn't get up to check out the window. -Pulling a dress out of the basket, Sandra looks up to see +Pulling a dress out of the basket, Sandra looks up to see -A MAN'S SILHOUETTE +A MAN'S SILHOUETTE on the sheet in front of her. She freezes, watching the shadow ripple across the white fabric, blowing so softly in the breeze. She knows it can't be him. He's dead. @@ -2931,7 +2931,7 @@ CROSSFADE TO: BRIGHT SUNLIGHT -filters through soft sheets. We're under the covers, where a man's hand traces the curves of a woman's bare back. A beat, then she turns over in bed, revealing her to be +filters through soft sheets. We're under the covers, where a man's hand traces the curves of a woman's bare back. A beat, then she turns over in bed, revealing her to be JOSEPHINE. @@ -2959,7 +2959,7 @@ JOSEPHINE What about the details? How they fell in love. The Circus. The War. You never told me any of that. WILL -That's because most of it never happened. +That's because most of it never happened. JOSEPHINE But it's romantic. @@ -2997,7 +2997,7 @@ JOSEPHINE But it's not true. WILL -What is "true?" I've never heard my father say a single true thing. +What is "true?" I've never heard my father say a single true thing. Off her silence... @@ -3015,7 +3015,7 @@ I am always on your side. And I think you should talk to him. INT. GUEST ROOM - DAY -The family finishes eating breakfast off TV trays set up around the bed. For his part, Edward is looking better. Certainly not recovered, but there's an optimism to his expression. And for the first time, he's actually hungry. +The family finishes eating breakfast off TV trays set up around the bed. For his part, Edward is looking better. Certainly not recovered, but there's an optimism to his expression. And for the first time, he's actually hungry. He watches as Sandra puts the cap back on the syrup. @@ -3054,7 +3054,7 @@ Son? WILL Can we talk? -Sandra SNAPS the cap back on the syrup. +Sandra SNAPS the cap back on the syrup. SANDRA I'm going to get started on dishes. @@ -3062,7 +3062,7 @@ I'm going to get started on dishes. JOSEPHINE I'll help you. -Both women quickly gather plates. +Both women quickly gather plates. Will and Edward both smile. The women clearly want this to happen. It settles for a beat after they leave. @@ -3108,7 +3108,7 @@ You think I'm fake. WILL Only on the surface. But that's all I've ever seen. -Edward looks away, angry and disbelieving. +Edward looks away, angry and disbelieving. WILL Dad, I'm about to have a kid of my own here. It would kill me if he went through his whole life never understanding me. @@ -3122,16 +3122,16 @@ EDWARD What do you want, Will? Who do you want me to be? WILL -Yourself. Good, bad, everything. Just show me who you are for once. +Yourself. Good, bad, everything. Just show me who you are for once. EDWARD I have been nothing but myself since the day I was born. And if you can't see that, it's your failing, not mine. EXT. BACKYARD - DAY -With a skimmer pole, Will cleans the leaves and debris out of the pool, but it's a fool's errand. The pool has long since gone native, a shiny slick of algae on the surface, slime covering the cemented rocks. +With a skimmer pole, Will cleans the leaves and debris out of the pool, but it's a fool's errand. The pool has long since gone native, a shiny slick of algae on the surface, slime covering the cemented rocks. -Suddenly, an underwater shape RIPPLES against the water's surface. Will is so startled that he drops the pole, which disappears into the murky water. +Suddenly, an underwater shape RIPPLES against the water's surface. Will is so startled that he drops the pole, which disappears into the murky water. A beat. He looks around, relieved that no one saw that. He casually walks away. @@ -3195,7 +3195,7 @@ Edward shakes hands with his new boss, a TOUPEED MAN. The company is called "Co EDWARD (V.O.) With my prospects few, I took a job as a travelling salesman. It suited me. If there's one thing you can say about Edward Bloom, it's that I am a social person. -EXT. COUNTY FAIR - DAY [STORY] +EXT. COUNTY FAIR - DAY [STORY] On a low platform, Edward pitches a brilliant new product to the crowd. @@ -3217,7 +3217,7 @@ Indeed, a remarkably agile Edward is able to support his entire weight on it. T EDWARD You can use it to point out important information. Or dangers. Or beautiful women. -The hand points a finger at an HEAVYSET MAN IN OVERALLS. +The hand points a finger at an HEAVYSET MAN IN OVERALLS. EDWARD We're still working on that one. @@ -3231,20 +3231,20 @@ Edward drives, his hand out in the wind. EDWARD (V.O.) Soon I added other products, and other cities, until my territory stretched from the coast to western Texas. -EXT. TRAILER PARK - DAY +EXT. TRAILER PARK - DAY Edward kisses his pregnant wife goodbye, as much in love as ever. EDWARD (V.O.) I could be gone for weeks at a time. But every other Friday, I'd put all the money I'd made into an account set aside for a proper house with a white picket fence. -EXT. HORIZON SAVINGS & LOAN - DAY +EXT. HORIZON SAVINGS & LOAN - DAY Establishing this Texas institution, we come... -INT. HORIZON SAVINGS & LOAN - DAY +INT. HORIZON SAVINGS & LOAN - DAY -The bank is busy with the lunch-hour crowd. Taking his place in line, Edward fills out a deposit slip. +The bank is busy with the lunch-hour crowd. Taking his place in line, Edward fills out a deposit slip. As the line snakes around through the ropes, the man in front of him gets a look at Edward. @@ -3300,7 +3300,7 @@ ANGLE ON Edward, not sure what to do. He has a gun, but he truly doesn't want t He decides he better go. -INT. AT THE VAULT - DAY +INT. AT THE VAULT - DAY The Teller Woman is crying as she works the combination. Edward feels horrible. @@ -3312,12 +3312,12 @@ It's not that, it's just... She pulls open the vault door. -INT. THE VAULT - DAY +INT. THE VAULT - DAY The inner sanctum of the Horizon Savings and Loan holds exactly one folding chair. Nothing else. TELLER WOMAN -...there's no money. We're completely bankrupt. +...there's no money. We're completely bankrupt. EDWARD (V.O.) It turned out the savings and loan had already been robbed -- not by armed bandits, but by speculators in Texas real estate. @@ -3328,7 +3328,7 @@ You gotta promise you won't tell anybody. CUT TO: -INT. EDWARD'S CAR - DAY +INT. EDWARD'S CAR - DAY Edward drives the getaway car, though truthfully they're going just a little over the speed limit. No one's following them. It's an empty country road for miles. @@ -3358,7 +3358,7 @@ Edward continues his narration... EDWARD (V.O.) I told Norther about the vagaries of Texas oil money and its effect on real estate prices, and how lax enforcement of fiduciary process had made savings and loans particularly vulnerable. Hearing this news, Norther was left with one conclusion: -EXT. TEXAS ROAD - DAY +EXT. TEXAS ROAD - DAY Norther leans in the driver's side window. @@ -3368,7 +3368,7 @@ I should go to Wall Street. That's where all the money is. Edward looks over at Norther, the reality sinking in. EDWARD (V.O.) -I knew then that while my days as a criminal were over, Norther's were just beginning. +I knew then that while my days as a criminal were over, Norther's were just beginning. The two men wave at each other as Edward drives off. At the last moment, Norther calls out: @@ -3380,7 +3380,7 @@ He's talking about his Hand Around the House. We HOLD ON Norther for a beat, dr EDWARD (V.O.) When Norther made his first million dollars, he sent me a check for ten thousand. I protested, but he said it was my fee as his career advisor. -EXT. BLOOM HOUSE [MID/LATE '70'S] - DAY +EXT. BLOOM HOUSE [MID/LATE '70'S] - DAY Sandra is watering the garden. Will (5) runs past her to greet Edward, just returned from another trip. @@ -3396,7 +3396,7 @@ Sandra drops the hose, letting it run on the lawn. TRANSITION TO: -INT. BLOOM HOUSE BATHROOM - DAY [PRESENT] +INT. BLOOM HOUSE BATHROOM - DAY [PRESENT] CLOSE ON Edward's hand as he turns knobs. @@ -3404,15 +3404,15 @@ CLOSE ON water SPLASHING into the claw-foot bathtub, which begins to fill. Still wearing his pajamas, Edward climbs into the tub. Carefully lowers himself. -As the water reaches the third button up on his pajama shirt, Edward suddenly slides +As the water reaches the third button up on his pajama shirt, Edward suddenly slides UNDERWATER. Bubbles rise from his nose for a few beats, then stop. It's quiet, except for the distant SPLASHING of water from the spigot. Edward's eyes are closed. -A long beat. Another. Then the SPLASHING water goes silent. Edward opens one eye. The other eye. He sits up to find +A long beat. Another. Then the SPLASHING water goes silent. Edward opens one eye. The other eye. He sits up to find -SANDRA +SANDRA sitting on the edge of the tub. She doesn't seem particularly worried -- her husband has always done this. @@ -3553,7 +3553,7 @@ You don't have to tell him twice. He's out the door in a flash. INT. JENNY'S KITCHEN - DAY -While Will sips his iced tea, Jenny flips through a form she never expected to see again. She hands it back to Will. +While Will sips his iced tea, Jenny flips through a form she never expected to see again. She hands it back to Will. WILL How did you know my father? @@ -3594,14 +3594,14 @@ Fair enough. Jenny takes a seat across from him at the table. JENNY The first thing you have to understand, is that your father never meant to end up here. And yet he did, twice. The first time, he was early. The second time, he was late. -INT. EDWARD'S CAR / DRIVING - NIGHT +INT. EDWARD'S CAR / DRIVING - NIGHT -It's late, and Edward is pensive. +It's late, and Edward is pensive. JENNY (V.O.) Those days, your father was working for himself. If there was one thing you could say about Edward Bloom, it's that he was a social person, and people took a liking to him. One night he was returning from three weeks on the road, when he hit a thunderstorm unlike any in his life. -The first raindrops hit the windshield. Edward turns on the wipers. +The first raindrops hit the windshield. Edward turns on the wipers. INT. EDWARD'S CAR - NIGHT - [THE STORM] @@ -3609,17 +3609,17 @@ Suddenly, a deluge descends. It's not even rain anymore -- there's no space bet No choice, Edward stops the car. Puts on the handbrake. Just as suddenly, the sound changes -- no longer pounding, but softly SPLASHING. The world is close and echoing, because -- -EXT. EDWARD'S CAR - NIGHT +EXT. EDWARD'S CAR - NIGHT -- the car is underwater. The tires are still on the road, but where there used to be air is water. Three catfish swim in front of his headlights. -INT. THE CAR - NIGHT +INT. THE CAR - NIGHT Realizing his plight, Edward tries to remain calm. Water is trickling in through the crack between the window and the door, but very slowly. For now, he's fine. -That's when he sees her -- The Girl in the River. +That's when he sees her -- The Girl in the River. She's swimming outside the car. While we never see her face exactly, she remains just as beautiful, just as mysterious, as the first time we saw her. @@ -3631,7 +3631,7 @@ EXT. FIELD BY DIRT ROAD - DAY It's morning, and the sun shines brightly. Birds CHIRP. Trees drip and the grass shines, still wet from last night's rain. -Edward gathers the clothes that have spilled out of his suitcase, which broke open when he dropped it from +Edward gathers the clothes that have spilled out of his suitcase, which broke open when he dropped it from HIS CAR, @@ -3640,7 +3640,7 @@ which balances precariously ten feet up in an elm tree. As Edward gathers his l It's a key. It's the Key to the City he lost years ago. JENNY (V.O.) -Fate has a way of circling back on a man, and taking him by surprise. +Fate has a way of circling back on a man, and taking him by surprise. EXT. ROAD - DAY @@ -3653,14 +3653,14 @@ EXT. MAIN STREET - DAY Amazed and disbelieving, Edward walks down the center of the road, no cars coming from either direction. He looks up to find his faded shoes still dangling from the power line, along with the rest of the town's. JENNY (V.O.) -A man sees things differently at different times in his life. This town didn't seem the same now that he was older. +A man sees things differently at different times in his life. This town didn't seem the same now that he was older. EXT. TOWN OF SPECTRE - VARIOUS SHOTS We look around the town, on and off Main Street. There are "FOR SALE" signs in many of the windows JENNY (V.O.) -A new road had brought the outside world to Spectre, and with it, banks, liens and debt. Almost everywhere you looked, people were bankrupt. +A new road had brought the outside world to Spectre, and with it, banks, liens and debt. Almost everywhere you looked, people were bankrupt. EXT. SPECTRE - DAY @@ -3679,19 +3679,19 @@ Everyone turns to look at this new bidder. JENNY (V.O.) And so Edward Bloom decided to buy the town, in order to save it. -INT. NORTHER WINSLOW'S MANHATTAN - DAY +INT. NORTHER WINSLOW'S MANHATTAN - DAY Edward pitches his plan to Norther. JENNY (V.O.) He was never a wealthy man, but he had made other men rich, and now he asked for their favors. -INT./EXT. VARIOUS LOCATIONS - DAY +INT./EXT. VARIOUS LOCATIONS - DAY VARIOUS SHOTS: Expressive and passionate as always, Edward talks to Ping, Jing and Amos Calloway. JENNY (V.O.) -Most of them had never seen Spectre -- they only had Edward's words to describe it. That's all they needed. He sold them on the dream. +Most of them had never seen Spectre -- they only had Edward's words to describe it. That's all they needed. He sold them on the dream. JING You can structure it as a historical trust. But you'll need every contiguous piece of property. It's all or nothing. @@ -3710,7 +3710,7 @@ Whatever he bought, the people were not asked to leave or pay rent or anything. EXT. ROAD IN THE SWAMP - DAY -Edward climbs out of his car, the road having literally stopped. The sun is shining, but it can barely penetrate the trees' thick canopy. +Edward climbs out of his car, the road having literally stopped. The sun is shining, but it can barely penetrate the trees' thick canopy. JENNY (V.O.) Within six months, his trust had purchased the entire town. With one exception. @@ -3737,7 +3737,7 @@ JENNY No one would come out here unless they had business. And no one would have business with me except for you. You're buying the town. EDWARD -Apparently I've overlooked this one piece of it, and I'd like to remedy that. You see, in order for the town to be preserved, the trust must own it in its entirety. +Apparently I've overlooked this one piece of it, and I'd like to remedy that. You see, in order for the town to be preserved, the trust must own it in its entirety. JENNY So I've heard. @@ -3761,7 +3761,7 @@ Then I don't think so Mr. Bloom. If nothing is going to change, I'd just as soo EDWARD It's not like you're going to lose anything. You can ask anyone in town. I've been nothing if not generous. I want the best for everyone. -A long beat. +A long beat. JENNY Mr. Bloom, why are you buying this land? Some sort of midlife crisis? Instead of buying a convertible, you buy a town? @@ -3790,7 +3790,7 @@ Young Jenny Hill watches barefoot Edward leave Spectre for the first time. BACK TO: -INT. SHACK - DAY +INT. SHACK - DAY Finally realizing who this woman is... @@ -3854,7 +3854,7 @@ Most men in that situation would accept their failure and move on. But Edward w EXT. SWAMP SHACK ROAD - ANOTHER DAY -With Karl the Giant's help, Edward unloads a brand new door from a pickup truck. +With Karl the Giant's help, Edward unloads a brand new door from a pickup truck. EXT. SHACK - DAY @@ -3869,7 +3869,7 @@ As the months passed, he found more and more things to fix, until the shack no l EXT. SWAMP - ANOTHER DAY -Edward and Karl cut down a tree, letting in a flood of light. Through the golden pollen hanging in the air, we RISE UP to see the shack is now +Edward and Karl cut down a tree, letting in a flood of light. Through the golden pollen hanging in the air, we RISE UP to see the shack is now THE LOVELY HOUSE @@ -3894,7 +3894,7 @@ He takes his hat off the rack. JENNY You can leave it there. -A beat. Does she really mean it? +A beat. Does she really mean it? She comes closer. Edward holds his ground. She takes his hand, lifting his hat up to the peg. She's very close -- just a half-inch from kissing him when -- @@ -3914,7 +3914,7 @@ JENNY I know. EDWARD -And from the moment I saw her until the moment I die, she's the only one. +And from the moment I saw her until the moment I die, she's the only one. JENNY Lucky girl. @@ -3966,7 +3966,7 @@ WILL You didn't become crazy. JENNY -Well, therapy. And one day I realized I was in love with a man who could never love me back. I was living in a fairy tale. +Well, therapy. And one day I realized I was in love with a man who could never love me back. I was living in a fairy tale. Will smiles to hear it called that. @@ -3976,19 +3976,19 @@ People aren't like they are in stories. They hurt each other without meaning to Her composure is starting to break. WILL -No, I wanted to know. I'm glad I know. +No, I wanted to know. I'm glad I know. A long beat, both staring at their iced tea. Jenny is working herself into more of a state by not talking. JENNY I wanted to meet you for the longest time. I did. (a smile) -I envied you so much. The way Eddie would talk about you when you were at Missouri, that award you won. Congratulations, incidentally. And when you got the job at the A.P., everything, he was so proud of you. I mean, that's the thing. Every moment he loved you. +I envied you so much. The way Eddie would talk about you when you were at Missouri, that award you won. Congratulations, incidentally. And when you got the job at the A.P., everything, he was so proud of you. I mean, that's the thing. Every moment he loved you. She's fighting tears, not the first ones she's shed over this. JENNY (CONT'D) -And as brightly as the sun would shine when he was with me, every time he left it disappeared. I wanted to be as important to him as you were, and I was never going to be. I was make-believe and his other life, you, were real. +And as brightly as the sun would shine when he was with me, every time he left it disappeared. I wanted to be as important to him as you were, and I was never going to be. I was make-believe and his other life, you, were real. ANGLE ON Will, sorting through his swirling thoughts. @@ -4067,11 +4067,11 @@ No. He won't. I'm sorry. And like that, it's done. We HOLD ON Will, reeling from the news. -INT. HOSPITAL ROOM - NIGHT +INT. HOSPITAL ROOM - NIGHT Edward sleeps peacefully, just an oxygen tube under his nose. There are no beeping monitors, no blinking lights. It's mercifully quiet. -Sandra squeezes Will's hand tightly. She's holding herself together, but it's been a tough day. +Sandra squeezes Will's hand tightly. She's holding herself together, but it's been a tough day. Dr. Bennett has just gone through the details for the third time. @@ -4111,7 +4111,7 @@ She neatens his hair. Holds his hand. As she kisses his fingers, she tweaks he TRANSITION TO: -INT. HOSPITAL HALLWAY - NIGHT +INT. HOSPITAL HALLWAY - NIGHT Sandra waits outside the women's restroom. Her face is a study in strained composure -- acknowledging the inevitable but refusing to surrender to it. @@ -4148,7 +4148,7 @@ A beat, then Sandra finally melts. Josephine holds her. The two women stand to INT. HOSPITAL ROOM - NIGHT -Will sits in a chair beside the bed, working through the crossword puzzle. A KNOCK as Dr. Bennett enters with his overcoat and bag, ready to leave for the night. +Will sits in a chair beside the bed, working through the crossword puzzle. A KNOCK as Dr. Bennett enters with his overcoat and bag, ready to leave for the night. DR. BENNETT Glad to see you're not trying to have a heartfelt talk. It's one of my greatest annoyances, when people talk to those who can't hear them. @@ -4175,7 +4175,7 @@ How would his son describe him? Tables turned, Will searches for an answer. He doesn't have one. -Dr. Bennett hangs the chart back on the bed. +Dr. Bennett hangs the chart back on the bed. DR. BENNETT (CONT'D) Did your father ever tell you about the day you were born? @@ -4210,9 +4210,9 @@ He smiles, remembering something. On his fourth page, he looks up at his motion CROSSFADE TO: -INT. HOSPITAL ROOM - PRE-DAWN +INT. HOSPITAL ROOM - PRE-DAWN -It's very early morning, and the first blue light of day is glowing through the vertical blinds. Still in his chair, Will wakes up a bit at a time. The notepad is on his lap, the pen in his hand. +It's very early morning, and the first blue light of day is glowing through the vertical blinds. Still in his chair, Will wakes up a bit at a time. The notepad is on his lap, the pen in his hand. He cracks his neck, crooked from sleeping on it wrong. What woke him up? @@ -4226,7 +4226,7 @@ His father is awake, silently GASPING for breath. His eyes are open, scared and WILL (CONT'D) Dad! Do you want me to get a nurse? -Edward shakes his head unambiguously. Will already has his finger on the orange "nurse call" button, but doesn't push it. +Edward shakes his head unambiguously. Will already has his finger on the orange "nurse call" button, but doesn't push it. WILL (CONT'D) What can I do? Can I help? Can I get you something? Water? @@ -4278,7 +4278,7 @@ Like this. WILL Okay. Okay. -Will looks around the room, increasingly desperate. He looks to the nurse call button. He really wants to press it. +Will looks around the room, increasingly desperate. He looks to the nurse call button. He really wants to press it. ON EDWARD, waiting for Will to begin. @@ -4292,13 +4292,13 @@ INT. HOSPITAL ROOM - DAY [STORY VERSION] WILL Dad? -It's dawn, and the first golden glow is shining through the vertical blinds. +It's dawn, and the first golden glow is shining through the vertical blinds. WILL (CONT'D) (louder and concerned) Dad? -We LOOK OVER to find a nimble Edward sitting up in bed, combing his hair. +We LOOK OVER to find a nimble Edward sitting up in bed, combing his hair. EDWARD Let's get out of here. @@ -4359,7 +4359,7 @@ Will races Edward down the row, finally reaching the Chevrolet. AT THE CAR -Will lifts his father out of the chair. +Will lifts his father out of the chair. WILL (V.O.) I pick you up and you hardly weigh anything. I can't explain it. @@ -4417,7 +4417,7 @@ Edward leans out the window and waves. Karl waves back. EXT. ASHTON RIVER - DAY -The same stretch of the river where it all began. A CROWD of more than 100 waiting. +The same stretch of the river where it all began. A CROWD of more than 100 waiting. WILL (V.O.) As we get closer to the river, we see everybody's already there. And I mean everybody. @@ -4502,7 +4502,7 @@ INT. HOSPITAL ROOM - DAY Will has tears hanging in the corners of his eyes. WILL -You become what you always were. A very big fish. +You become what you always were. A very big fish. (he smiles) And that's the way it happens. @@ -4510,7 +4510,7 @@ EDWARD (a whisper) Yes. Exactly. -Edward smiles, proud of both of them. His eyes are so pale and so open, we can almost see his soul. In every atom of his body, in every thought, Edward Bloom is entirely happy. +Edward smiles, proud of both of them. His eyes are so pale and so open, we can almost see his soul. In every atom of his body, in every thought, Edward Bloom is entirely happy. And this is how he goes. @@ -4540,7 +4540,7 @@ INT. BEDROOM - DAY Will and his mother pick out one of Edward's ties, for Will to wear at the funeral. Will tries to button the cuffs on the shirt he borrowed, but they're the kind that need links. -He goes through the top drawer of the dresser, trying to find a matching pair. Further down, he finds a ribbon tied to +He goes through the top drawer of the dresser, trying to find a matching pair. Further down, he finds a ribbon tied to THE KEY TO THE CITY. @@ -4548,9 +4548,9 @@ He smiles, disbelieving. It's a real thing. EXT. CEMETERY / ROAD - DAY -Will helps his mother out of a black sedan. She's well-composed, not nearly the wreck we might have expected. +Will helps his mother out of a black sedan. She's well-composed, not nearly the wreck we might have expected. -Josephine hugs Dr. Bennett and shakes hands with his WIFE. The service is crowded, more than 200 people, many more than expected. +Josephine hugs Dr. Bennett and shakes hands with his WIFE. The service is crowded, more than 200 people, many more than expected. As his mother talks to a WELL-WISHER, Will looks left to see an Oldsmobile parking. @@ -4564,13 +4564,13 @@ The Driver climbs out, a size 15 foot on the gravel. We TILT UP to see this man This man is KARL, now 55. He's not 12 feet tall, but at least six-eight. -CLOSE ON Will, bewildered to see that this man really exists. +CLOSE ON Will, bewildered to see that this man really exists. EXT. CEMETERY / GRAVESIDE - DAY As the service gets ready to begin, Will guides his mother to a seat near the grave. -Sitting beside Will, Josephine spots a stunning ASIAN WOMAN (50) behind them. A beat later, an identical face with glasses peers out -- the woman's twin sister. +Sitting beside Will, Josephine spots a stunning ASIAN WOMAN (50) behind them. A beat later, an identical face with glasses peers out -- the woman's twin sister. It's PING and JING. @@ -4589,7 +4589,7 @@ Have you ever heard a joke so many times you've forgotten why it's funny? But t Will joins in, laughing. -We slowly CIRCLE BEHIND a monument, letting it black out the screen. +We slowly CIRCLE BEHIND a monument, letting it black out the screen. TRANSITION TO: diff --git a/pyproject.toml b/pyproject.toml index fa3a148..1b961d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,4 +36,36 @@ screenplain = "screenplain.main:cli" dev = [ "pytest>=8.4.2", "ruff>=0.14.11", + "setuptools>=82.0.1", + "ty>=0.0.53", ] + +[tool.ruff] +src = ["./"] + +[tool.ruff.lint] +select = [ + "A", + "B", + "ANN201", + "F", + "I", + "T20", + "TID", + "RUF", + "UP", +] +ignore = [ + "RUF010", + "T20" +] + +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ty.rules] +all = "error" + +[tool.ty.src] +include = ["./"] +exclude = ["./**/*.fdx", "./**/*.fountain"] diff --git a/screenplain/export/courier_prime/LICENSE/OFL-FAQ.txt b/screenplain/export/courier_prime/LICENSE/OFL-FAQ.txt index edb55c6..b49a49f 100644 --- a/screenplain/export/courier_prime/LICENSE/OFL-FAQ.txt +++ b/screenplain/export/courier_prime/LICENSE/OFL-FAQ.txt @@ -79,7 +79,7 @@ The intent is that the goodwill and reputation of the author(s) should not be us 2 USING OFL FONTS FOR WEBPAGES AND ONLINE WEBFONT SERVICES 2.1 Can I make webpages using these fonts? -Yes! Go ahead! Using CSS (Cascading Style Sheets) is recommended. Your three best options: +Yes! Go ahead! Using CSS (Cascading Style Sheets) is recommended. Your three best options: - referring directly in your stylesheet to open fonts which may be available on the user's system - providing links to download the full package of the font - either from your own website or from elsewhere - so users can install it themselves - using @font-face to distribute the font directly to browsers. This is recommended and explicitly allowed by the licensing model because it is distribution. The font file itself is distributed with other components of the webpage. It is not embedded in the webpage but referenced through a web address which will cause the browser to retrieve and use the corresponding font to render the webpage (see 1.11 and 1.15 for details related to embedding fonts into documents). As you take advantage of the @font-face cross-platform standard, be aware that webfonts are often tuned for a web environment and not intended for installation and use outside a browser. The reasons in favour of using webfonts are to allow design of dynamic text elements instead of static graphics, to make it easier for content to be localized and translated, indexed and searched, and all this with cross-platform open standards without depending on restricted extensions or plugins. You should check the CSS cascade (the order in which fonts are being called or delivered to your users) when testing. @@ -101,9 +101,9 @@ In most cases these formats alter the original font data more than WOFF, and do Yes, you are welcome to include OFL fonts in online webfont services as long as you properly meet all the conditions of the license. The origin and open status of the font should be clear among the other fonts you are hosting. Authorship, copyright notices and license information must be sufficiently visible to your users or subscribers so they know where the font comes from and the rights granted by the author(s). Make sure the font file contains the needed copyright notice(s) and licensing information in its metadata. Please double-check the accuracy of every field to prevent contradictory information. Other font formats, including EOT/EOTLite/CWT and superior alternatives like WOFF, already provide fields for this information. Remember that if you modify the font within your library or convert it to another format for any reason the OFL restrictions apply and you need to change the names accordingly. Please respect the author's wishes as expressed in the OFL and do not misrepresent original designers and their work. Don't lump quality open fonts together with dubious freeware or public domain fonts. Consider how you can best work with the original designers and foundries, support their efforts and generate goodwill that will benefit your service. (See 1.17 for details related to URL-based access restrictions methods or DRM mechanisms). 2.5 Can I make and publish CMS themes or templates that use OFL fonts? Can I include the fonts themselves in the themes or templates? Can I sell the whole package? -Yes, you are very welcome to integrate open fonts into themes and templates for your preferred CMS and make them more widely available. Be aware that you can only sell the fonts and your CMS add-on as part of a software bundle. (See 1.4 for details and examples about selling bundles). +Yes, you are very welcome to integrate open fonts into themes and templates for your preferred CMS and make them more widely available. Be aware that you can only sell the fonts and your CMS add-on as part of a software bundle. (See 1.4 for details and examples about selling bundles). -2.6 Some webfont formats and services provide ways of "optimising" the font for a particular website or web application; is that allowed? +2.6 Some webfont formats and services provide ways of "optimising" the font for a particular website or web application; is that allowed? Yes, it is permitted, but remember that these optimised versions are Modified Versions and so must follow OFL requirements like appropriate renaming. Also you need to bear in mind the other important parameters beyond compression, speed and responsiveness: you need to consider the audience of your particular website or web application, as choosing some optimisation parameters may turn out to be less than ideal for them. Subsetting by removing certain glyphs or features may seriously limit functionality of the font in various languages used by your users. It may also introduce degradation of quality in the rendering or specific bugs on the various platforms compared to the original font. In other words, remember that one person's optimised font may be another person's missing feature. Various advanced typographic features are also available through CSS and may provide the desired effects without the need to modify the font. @@ -163,15 +163,15 @@ No, although if the font is of sufficient quality and general interest we may in - to allow your font to be included in Libre Software operating systems like Ubuntu - to give your font world status and wide, unrestricted distribution - to educate students about quality typeface and font design -- to expand your test base and get more useful feedback +- to expand your test base and get more useful feedback - to extend your reach to new markets when users see your metadata and go to your website - to get your font more easily into one of the webfont online services - to attract attention for your commercial fonts - to make money through webfont services - to make money by bundling fonts with applications - to make money adjusting and extending existing open fonts -- to get a better chance that foundations/NGOs/charities/companies who commission fonts will pick you -- to be part of a sharing design and development community +- to get a better chance that foundations/NGOs/charities/companies who commission fonts will pick you +- to be part of a sharing design and development community - to give back and contribute to a growing body of font sources @@ -184,7 +184,7 @@ These are font names, or portions of font names, that the author has chosen to r The best way to acknowledge the source of the design is to thank the original authors and any other contributors in the files that are distributed with your revised font (although no acknowledgement is required). The FONTLOG is a natural place to do this. Reserved Font Names ensure that the only fonts that have the original names are the unmodified Original Versions. This allows designers to maintain artistic integrity while allowing collaboration to happen. It eliminates potential confusion and name conflicts. When choosing a name, be creative and avoid names that reuse almost all the same letters in the same order or sound like the original. It will help everyone if Original Versions and Modified Versions can easily be distinguished from one another and from other derivatives. Any substitution and matching mechanism is outside the scope of the license. 5.3 What do you mean by "primary name as presented to the user"? Are you referring to the font menu name? -Yes, this applies to the font menu name and other mechanisms that specify a font in a document. It would be fine, however, to keep a text reference to the original fonts in the description field, in your modified source file or in documentation provided alongside your derivative as long as no one could be confused that your modified source is the original. But you cannot use the Reserved Font Names in any way to identify the font to the user (unless the Copyright Holder(s) allow(s) it through a separate agreement). Users who install derivatives (Modified Versions) on their systems should not see any of the original Reserved Font Names in their font menus, for example. Again, this is to ensure that users are not confused and do not mistake one font for another and so expect features only another derivative or the Original Version can actually offer. +Yes, this applies to the font menu name and other mechanisms that specify a font in a document. It would be fine, however, to keep a text reference to the original fonts in the description field, in your modified source file or in documentation provided alongside your derivative as long as no one could be confused that your modified source is the original. But you cannot use the Reserved Font Names in any way to identify the font to the user (unless the Copyright Holder(s) allow(s) it through a separate agreement). Users who install derivatives (Modified Versions) on their systems should not see any of the original Reserved Font Names in their font menus, for example. Again, this is to ensure that users are not confused and do not mistake one font for another and so expect features only another derivative or the Original Version can actually offer. 5.4 Am I not allowed to use any part of the Reserved Font Names? You may not use individual words from the Reserved Font Names, but you would be allowed to use parts of words, as long as you do not use any word from the Reserved Font Names entirely. We do not recommend using parts of words because of potential confusion, but it is allowed. For example, if "Foobar" was a Reserved Font Name, you would be allowed to use "Foo" or "bar", although we would not recommend it. Such an unfortunate choice would confuse the users of your fonts as well as make it harder for other designers to contribute. @@ -270,7 +270,7 @@ SIL gives permission to publish unofficial translations into other languages pro "This is an unofficial translation of the SIL Open Font License into . It was not published by SIL International, and does not legally state the distribution terms for fonts that use the OFL. A release under the OFL is only valid when using the original English text. However, we recognize that this unofficial translation will help users and designers not familiar with English to better understand and use the OFL. We encourage designers who consider releasing their creation under the OFL to read the OFL-FAQ in their own language if it is available. Please go to http://scripts.sil.org/OFL for the official version of the license and the accompanying OFL-FAQ." -- Keep your unofficial translation current and update it at our request if needed, for example if there is any ambiguity which could lead to confusion. +- Keep your unofficial translation current and update it at our request if needed, for example if there is any ambiguity which could lead to confusion. If you start such a unofficial translation effort of the OFL and OFL-FAQ please let us know. @@ -327,7 +327,7 @@ ChangeLog - Corrected minor typos in the documentation - Fixed position of combining inverted breve below (U+032F) - Added OpenType/Graphite smart code for Armenian -- Added Armenian glyphs (U+0531 -> U+0587) +- Added Armenian glyphs (U+0531 -> U+0587) - Released as "NewWorldFontFamily" 1 Jan 2005 (Joe Smith) GlobalFontFamily Version 1.0 diff --git a/screenplain/export/courier_prime/LICENSE/OFL.txt b/screenplain/export/courier_prime/LICENSE/OFL.txt index 9c9952a..847d4d7 100644 --- a/screenplain/export/courier_prime/LICENSE/OFL.txt +++ b/screenplain/export/courier_prime/LICENSE/OFL.txt @@ -19,7 +19,7 @@ with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, +fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The diff --git a/screenplain/export/courier_prime/Read me.txt b/screenplain/export/courier_prime/Read me.txt index c49013f..47cccd4 100644 --- a/screenplain/export/courier_prime/Read me.txt +++ b/screenplain/export/courier_prime/Read me.txt @@ -1,11 +1,11 @@ ABOUT COURIER PRIME =================== -Courier Prime is a TrueType monospaced font designed specifically for screenplays. It was designed by Alan Dague-Greene for John August and released by Quote-Unquote Apps under the SIL Open Font License (OFL). +Courier Prime is a TrueType monospaced font designed specifically for screenplays. It was designed by Alan Dague-Greene for John August and released by Quote-Unquote Apps under the SIL Open Font License (OFL). Visit http://quoteunquoteapps.com/courierprime for more information and the latest updates. -SIL Open Font License: http://scripts.sil.org/OFL +SIL Open Font License: http://scripts.sil.org/OFL INSTALLATION ============ diff --git a/screenplain/export/fdx.py b/screenplain/export/fdx.py index ccb52b0..c6bfdfb 100644 --- a/screenplain/export/fdx.py +++ b/screenplain/export/fdx.py @@ -14,16 +14,16 @@ ) style_names = { - Bold: 'Bold', - Italic: 'Italic', - Underline: 'Underline', + Bold: "Bold", + Italic: "Italic", + Underline: "Underline", } def _write_text_element(out, styles, text): - style_value = '+'.join(str(s) for s in styles) - if style_value == '': - out.write(f' {escape(text)}\n') + style_value = "+".join(str(s) for s in styles) + if style_value == "": + out.write(f" {escape(text)}\n") else: out.write(f' {escape(text)}\n') @@ -33,7 +33,7 @@ def write_text(out, rich, trailing_linebreak): for seg_no, segment in enumerate(rich.segments): fdx_styles = [style_names[n] for n in segment.get_ordered_styles()] if trailing_linebreak and seg_no == len(rich.segments) - 1: - _write_text_element(out, fdx_styles, segment.text + '\n') + _write_text_element(out, fdx_styles, segment.text + "\n") else: _write_text_element(out, fdx_styles, segment.text) @@ -47,38 +47,31 @@ def write_paragraph(out, para_type, lines, centered=False): last_line_no = len(lines) - 1 for line_no, line in enumerate(lines): write_text(out, line, line_no != last_line_no) - out.write(' \n') + out.write(" \n") def write_dialog(out, dialog): - write_paragraph(out, 'Character', [dialog.character]) + write_paragraph(out, "Character", [dialog.character]) for parenthetical, line in dialog.blocks: if parenthetical: - write_paragraph(out, 'Parenthetical', [line]) + write_paragraph(out, "Parenthetical", [line]) else: - write_paragraph(out, 'Dialogue', [line]) + write_paragraph(out, "Dialogue", [line]) def write_dual_dialog(out, dual): - out.write( - ' \n' - ' \n' - ) + out.write(" \n \n") write_dialog(out, dual.left) write_dialog(out, dual.right) - out.write( - ' \n' - ' \n' - ) + out.write(" \n \n") def to_fdx(screenplay, out): - out.write( '\n' '\n' - '\n' - ' \n' + "\n" + " \n" ) for para in screenplay: @@ -87,16 +80,13 @@ def to_fdx(screenplay, out): elif isinstance(para, DualDialog): write_dual_dialog(out, para) elif isinstance(para, Action): - write_paragraph(out, 'Action', para.lines, centered=para.centered) + write_paragraph(out, "Action", para.lines, centered=para.centered) elif isinstance(para, Slug): - write_paragraph(out, 'Scene Heading', para.lines) + write_paragraph(out, "Scene Heading", para.lines) elif isinstance(para, Transition): - write_paragraph(out, 'Transition', para.lines) + write_paragraph(out, "Transition", para.lines) else: # Ignore unknown types pass - out.write( - ' \n' - '\n' - ) + out.write(" \n\n") diff --git a/screenplain/export/html.py b/screenplain/export/html.py index cad1735..722f099 100644 --- a/screenplain/export/html.py +++ b/screenplain/export/html.py @@ -42,6 +42,7 @@ class tag: """ + def __init__(self, out, tag, classes=None): self.out = out self.tag = tag @@ -51,18 +52,18 @@ def __enter__(self): if self.classes: self.out.write(f'<{self.tag} class="{" ".join(self.classes)}">') else: - self.out.write(f'<{self.tag}>') + self.out.write(f"<{self.tag}>") def __exit__(self, exception_type, value, traceback): if not exception_type: - self.out.write(f'') + self.out.write(f"") return False def to_html(text): html = text.to_html() - if html == '': - return ' ' + if html == "": + return " " else: return html @@ -100,63 +101,63 @@ def convert(self, screenplay): format_function = self._format_functions.get(type(para), None) if format_function: format_function(para) - self.out.write('\n') + self.out.write("\n") def format_dialog(self, dialog): - with self._tag('div', classes=['dialog']): + with self._tag("div", classes=["dialog"]): self._write_dialog_block(dialog) def format_dual(self, dual): - with self._tag('div', classes=['dual']): - with self._tag('div', classes=['left']): + with self._tag("div", classes=["dual"]): + with self._tag("div", classes=["left"]): self._write_dialog_block(dual.left) - with self._tag('div', classes=['right']): + with self._tag("div", classes=["right"]): self._write_dialog_block(dual.right) - self.out.write('
') + self.out.write("
") def _write_dialog_block(self, dialog): - with self._tag('p', classes=['character']): + with self._tag("p", classes=["character"]): self.out.write(to_html(dialog.character)) for parenthetical, text in dialog.blocks: - classes = ['parenthetical'] if parenthetical else None - with self._tag('p', classes=classes): + classes = ["parenthetical"] if parenthetical else None + with self._tag("p", classes=classes): self.out.write(to_html(text)) def format_slug(self, slug): num = slug.scene_number - with self._tag('h6'): + with self._tag("h6"): if num: - with self._tag('span', classes=['scnuml']): + with self._tag("span", classes=["scnuml"]): self.out.write(to_html(slug.scene_number)) self.out.write(to_html(slug.line)) if num: - with self._tag('span', classes=['scnumr']): + with self._tag("span", classes=["scnumr"]): self.out.write(to_html(slug.scene_number)) if slug.synopsis: - with self._tag('span', classes=['h6-synopsis']): + with self._tag("span", classes=["h6-synopsis"]): self.out.write(to_html(plain(slug.synopsis))) def format_section(self, section): - with self._tag(f'h{section.level}'): + with self._tag(f"h{section.level}"): self.out.write(to_html(section.text)) if section.synopsis: - with self._tag('span', classes=[f'h{section.level}-synopsis']): + with self._tag("span", classes=[f"h{section.level}-synopsis"]): self.out.write(to_html(plain(section.synopsis))) def format_action(self, para): - classes = ['action'] + classes = ["action"] if para.centered: - classes.append('centered') - with self._tag('div', classes=classes): - with self._tag('p'): + classes.append("centered") + with self._tag("div", classes=classes): + with self._tag("p"): for number, line in enumerate(para.lines): if number != 0: - self.out.write('
') + self.out.write("
") self.out.write(to_html(line)) def format_transition(self, para): - with self._tag('div', classes=['transition']): + with self._tag("div", classes=["transition"]): self.out.write(to_html(para.line)) def format_page_break(self, para): @@ -165,7 +166,7 @@ def format_page_break(self, para): def _tag(self, tag_name, classes=[]): if self.page_break_before_next: self.page_break_before_next = False - classes = set(classes).union(('page-break',)) + classes = set(classes).union(("page-break",)) return tag(self.out, tag_name, classes) @@ -179,8 +180,9 @@ def convert(screenplay, out, css_file=None, bare=False): convert_bare(screenplay, out) else: convert_full( - screenplay, out, - css_file or os.path.join(os.path.dirname(__file__), 'default.css') + screenplay, + out, + css_file or os.path.join(os.path.dirname(__file__), "default.css"), ) @@ -189,28 +191,15 @@ def convert_full(screenplay, out, css_file): written to the file-like object `out`. """ - with open(css_file, encoding='utf-8') as stream: + with open(css_file, encoding="utf-8") as stream: css = stream.read() out.write( - '\n' - '' - '' - 'Screenplay' - '' - '' - '' - '
\n' - ) + out.write('
\n') convert_bare(screenplay, out) - out.write( - '
' - '' - '\n' - ) + out.write("
\n") def convert_bare(screenplay, out): diff --git a/screenplain/export/pdf.py b/screenplain/export/pdf.py index 6331178..f1d1e76 100644 --- a/screenplain/export/pdf.py +++ b/screenplain/export/pdf.py @@ -2,15 +2,17 @@ # Licensed under the MIT license: # http://www.opensource.org/licenses/mit-license.php -import sys import os import re +import sys from reportlab import platypus from reportlab.lib import colors, pagesizes from reportlab.lib.enums import TA_CENTER, TA_RIGHT from reportlab.lib.styles import ParagraphStyle from reportlab.lib.units import inch +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont from reportlab.platypus import ( BaseDocTemplate, Flowable, @@ -23,13 +25,11 @@ from screenplain import types from screenplain.types import Action, Dialog, DualDialog, Slug, Transition -from reportlab.pdfbase import pdfmetrics -from reportlab.pdfbase.ttfonts import TTFont try: import reportlab except ImportError: - sys.stderr.write('ERROR: ReportLab is required for PDF output\n') + sys.stderr.write("ERROR: ReportLab is required for PDF output\n") raise del reportlab @@ -50,14 +50,14 @@ def __init__(self, family_name): def register(self): if not self.file_normal: - raise RuntimeError('No font file set for normal typeface') + raise RuntimeError("No font file set for normal typeface") kwargs = {} - for (suffix, file, arg) in [ - ('', self.file_normal, 'normal'), - (' Bold', self.file_bold, 'bold'), - (' Italic', self.file_italic, 'italic'), - (' Bold Italic', self.file_bold_italic, 'boldItalic') + for suffix, file, arg in [ + ("", self.file_normal, "normal"), + (" Bold", self.file_bold, "bold"), + (" Italic", self.file_italic, "italic"), + (" Bold Italic", self.file_bold_italic, "boldItalic"), ]: if file: n = self.family_name + suffix @@ -73,14 +73,13 @@ def get_standard_font_settings() -> FontSettings: def get_courier_prime_settings() -> FontSettings: - """Get font settings for Courier Prime, which is bundled with Screenplain - """ - path = os.path.join(os.path.dirname(__file__), 'courier_prime') - s = FontSettings('Courier Prime') - s.file_normal = os.path.join(path, 'Courier Prime.ttf') - s.file_bold = os.path.join(path, 'Courier Prime Bold.ttf') - s.file_italic = os.path.join(path, 'Courier Prime Italic.ttf') - s.file_bold_italic = os.path.join(path, 'Courier Prime Bold Italic.ttf') + """Get font settings for Courier Prime, which is bundled with Screenplain""" + path = os.path.join(os.path.dirname(__file__), "courier_prime") + s = FontSettings("Courier Prime") + s.file_normal = os.path.join(path, "Courier Prime.ttf") + s.file_bold = os.path.join(path, "Courier Prime Bold.ttf") + s.file_italic = os.path.join(path, "Courier Prime Italic.ttf") + s.file_bold_italic = os.path.join(path, "Courier Prime Bold Italic.ttf") s.register() return s @@ -134,7 +133,7 @@ def __init__( characters_per_line=61, page_size=pagesizes.letter, strong_slugs=False, - font_settings=None + font_settings=None, ): self.font_settings = font_settings or get_courier_prime_settings() @@ -150,20 +149,14 @@ def __init__( self.frame_width = self.characters_per_line * self.character_width [self.page_width, self.page_height] = page_size self.left_margin = 1.5 * inch - self.right_margin = self.page_width - ( - self.left_margin + self.frame_width - ) + self.right_margin = self.page_width - (self.left_margin + self.frame_width) self.top_margin = 1 * inch - self.bottom_margin = self.page_height - ( - self.top_margin + self.frame_height - ) - self.title_frame_width = self.page_width - ( - self.left_margin + self.left_margin - ) + self.bottom_margin = self.page_height - (self.top_margin + self.frame_height) + self.title_frame_width = self.page_width - (self.left_margin + self.left_margin) self.strong_slugs = strong_slugs default_style = ParagraphStyle( - 'default', + "default", fontName=self.font_settings.family_name, fontSize=font_size, leading=line_height, @@ -175,64 +168,77 @@ def __init__( self.default_style = default_style self.centered_style = ParagraphStyle( - 'default-centered', self.default_style, + "default-centered", + self.default_style, alignment=TA_CENTER, ) self.character_style = ParagraphStyle( - 'character', default_style, + "character", + default_style, spaceBefore=line_height, leftIndent=19 * self.character_width, keepWithNext=1, ) self.dialog_style = ParagraphStyle( - 'dialog', default_style, + "dialog", + default_style, leftIndent=9 * self.character_width, rightIndent=self.frame_width - (45 * self.character_width), ) self.parenthentical_style = ParagraphStyle( - 'parenthentical', default_style, + "parenthentical", + default_style, leftIndent=13 * self.character_width, keepWithNext=1, ) self.action_style = ParagraphStyle( - 'action', default_style, + "action", + default_style, spaceBefore=line_height, ) self.centered_action_style = ParagraphStyle( - 'centered-action', self.action_style, + "centered-action", + self.action_style, alignment=TA_CENTER, ) self.slug_style = ParagraphStyle( - 'slug', default_style, + "slug", + default_style, spaceBefore=line_height, spaceAfter=line_height, keepWithNext=1, ) self.transition_style = ParagraphStyle( - 'transition', default_style, + "transition", + default_style, spaceBefore=line_height, spaceAfter=line_height, alignment=TA_RIGHT, ) self.title_style = ParagraphStyle( - 'title', default_style, - fontSize=font_size * 2, leading=font_size * 3, + "title", + default_style, + fontSize=font_size * 2, + leading=font_size * 3, alignment=TA_CENTER, ) self.contact_style = ParagraphStyle( - 'contact', default_style, + "contact", + default_style, ) - self.dual_dialog_table_style = platypus.TableStyle([ - ('VALIGN', (0, 0), (-1, -1), 'TOP'), - ('LEFTPADDING', (0, 0), (-1, -1), 0), - ('RIGHTPADDING', (0, 0), (-1, -1), 0), - ('TOPPADDING', (0, 0), (-1, -1), 0), - ('BOTTOMPADDING', (0, 0), (-1, -1), 0), - ('LINEBEFORE', (1, 0), (1, 0), 0, colors.white), - ]) + self.dual_dialog_table_style = platypus.TableStyle( + [ + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("LEFTPADDING", (0, 0), (-1, -1), 0), + ("RIGHTPADDING", (0, 0), (-1, -1), 0), + ("TOPPADDING", (0, 0), (-1, -1), 0), + ("BOTTOMPADDING", (0, 0), (-1, -1), 0), + ("LINEBEFORE", (1, 0), (1, 0), 0, colors.white), + ] + ) class SlugWithSceneNumbers(Flowable): @@ -260,10 +266,7 @@ def draw(self): canvas.saveState() # Use same font as slug, but always plain (not bold/underline) - canvas.setFont( - self.settings.font_settings.family_name, - self.settings.font_size - ) + canvas.setFont(self.settings.font_settings.family_name, self.settings.font_size) # Left margin: Position to the left of frame left_x = -0.75 * inch @@ -278,39 +281,41 @@ def draw(self): class DocTemplate(BaseDocTemplate): def __init__(self, *args, **kwargs): - self.settings = ( - kwargs.pop('settings', None) or create_default_settings() - ) - self.has_title_page = kwargs.pop('has_title_page', False) + self.settings = kwargs.pop("settings", None) or create_default_settings() + self.has_title_page = kwargs.pop("has_title_page", False) frame = Frame( self.settings.left_margin, self.settings.bottom_margin, self.settings.frame_width, self.settings.frame_height, - id='normal', - leftPadding=0, topPadding=0, rightPadding=0, bottomPadding=0 + id="normal", + leftPadding=0, + topPadding=0, + rightPadding=0, + bottomPadding=0, ) title_frame = Frame( self.settings.left_margin, self.settings.bottom_margin, self.settings.title_frame_width, self.settings.frame_height, - id='title', - leftPadding=0, topPadding=0, rightPadding=0, bottomPadding=0 + id="title", + leftPadding=0, + topPadding=0, + rightPadding=0, + bottomPadding=0, ) pageTemplates = [ - PageTemplate(id='title', frames=[title_frame]), - PageTemplate(id='standard', frames=[frame]) + PageTemplate(id="title", frames=[title_frame]), + PageTemplate(id="standard", frames=[frame]), ] - BaseDocTemplate.__init__( - self, pageTemplates=pageTemplates, *args, **kwargs - ) + BaseDocTemplate.__init__(self, pageTemplates=pageTemplates, *args, **kwargs) def handle_pageBegin(self): self.canv.setFont( self.settings.font_settings.family_name, self.settings.font_size, - leading=self.settings.line_height + leading=self.settings.line_height, ) if self.has_title_page: page = self.page # self.page is 0 on first page @@ -320,22 +325,19 @@ def handle_pageBegin(self): self.canv.drawRightString( self.settings.left_margin + self.settings.frame_width, self.settings.page_height - 42, - f'{page}.' + f"{page}.", ) self._handle_pageBegin() def add_paragraph(story, para, style): - story.append(Paragraph( - '
'.join(line.to_html() for line in para.lines), - style - )) + story.append(Paragraph("
".join(line.to_html() for line in para.lines), style)) def add_slug(story, para, settings): for line in para.lines: if settings.strong_slugs: - html = '' + line.to_html() + '' + html = "" + line.to_html() + "" else: html = line.to_html() @@ -343,9 +345,7 @@ def add_slug(story, para, settings): # Wrap in custom flowable if scene number exists if para.scene_number: - flowable = SlugWithSceneNumbers( - paragraph, para.scene_number, settings - ) + flowable = SlugWithSceneNumbers(paragraph, para.scene_number, settings) story.append(flowable) else: story.append(paragraph) @@ -359,29 +359,27 @@ def _dialog_to_flowables( proportion = column_width / settings.frame_width character_left = settings.character_style.leftIndent * proportion dialog_left = settings.dialog_style.leftIndent * proportion - parenthentical_left = (settings.parenthentical_style.leftIndent * - proportion) + parenthentical_left = settings.parenthentical_style.leftIndent * proportion dialog_right = settings.dialog_style.rightIndent * proportion character_style = ParagraphStyle( - 'character-dual', settings.character_style, - leftIndent=character_left + "character-dual", settings.character_style, leftIndent=character_left ) dialog_style = ParagraphStyle( - 'dialog-dual', settings.dialog_style, + "dialog-dual", + settings.dialog_style, leftIndent=dialog_left, - rightIndent=dialog_right + rightIndent=dialog_right, ) parenthentical_style = ParagraphStyle( - 'parenth-dual', settings.parenthentical_style, - leftIndent=parenthentical_left + "parenth-dual", + settings.parenthentical_style, + leftIndent=parenthentical_left, ) else: character_style = settings.character_style dialog_style = settings.dialog_style parenthentical_style = settings.parenthentical_style - flowables: list[Flowable] = [ - Paragraph(dialog.character.to_html(), character_style) - ] + flowables: list[Flowable] = [Paragraph(dialog.character.to_html(), character_style)] for is_parenthetical, line in dialog.blocks: if is_parenthetical: style = parenthentical_style @@ -405,13 +403,9 @@ def add_dialog(story, dialog, settings: Settings): def add_dual_dialog(story, dual, settings: Settings): # Format dual dialog side-by-side using a Table col_width = settings.frame_width / 2 - left_flowables = _dialog_to_flowables(dual.left, - settings, column_width=col_width) - right_flowables = _dialog_to_flowables(dual.right, - settings, column_width=col_width) - table_data = [ - [left_flowables, right_flowables] - ] + left_flowables = _dialog_to_flowables(dual.left, settings, column_width=col_width) + right_flowables = _dialog_to_flowables(dual.right, settings, column_width=col_width) + table_data = [[left_flowables, right_flowables]] # Create a table with one row and two columns. # Set splitInRow=1 to allow splitting the table across pages. table = platypus.Table( @@ -425,9 +419,7 @@ def add_dual_dialog(story, dual, settings: Settings): def get_title_page_story(screenplay, settings): - """Get Platypus flowables for the title page - - """ + """Get Platypus flowables for the title page""" # From Fountain spec: # The recommendation is that Title, Credit, Author (or Authors, either # is a valid key syntax), and Source will be centered on the page in @@ -446,51 +438,52 @@ def add_lines(story, attribute, style, space_before=0): for line in lines: html = line.to_html() para = Paragraph(html, style) - width, height = para.wrap( - settings.frame_width, settings.frame_height - ) + width, height = para.wrap(settings.frame_width, settings.frame_height) story.append(para) total_height += height return space_before + total_height title_story = [] - title_height = sum(( - add_lines(title_story, 'Title', settings.title_style), - add_lines( - title_story, - 'Credit', - settings.centered_style, - space_before=settings.line_height - ), - add_lines(title_story, 'Author', settings.centered_style), - add_lines(title_story, 'Authors', settings.centered_style), - add_lines(title_story, 'Source', settings.centered_style), - )) + title_height = sum( + ( + add_lines(title_story, "Title", settings.title_style), + add_lines( + title_story, + "Credit", + settings.centered_style, + space_before=settings.line_height, + ), + add_lines(title_story, "Author", settings.centered_style), + add_lines(title_story, "Authors", settings.centered_style), + add_lines(title_story, "Source", settings.centered_style), + ) + ) lower_story = [] - lower_height = sum(( - add_lines(lower_story, 'Draft date', settings.default_style), - add_lines( - lower_story, - 'Contact', - settings.contact_style, - space_before=settings.line_height - ), - add_lines( - lower_story, - 'Copyright', - settings.centered_style, - space_before=settings.line_height - ), - )) + lower_height = sum( + ( + add_lines(lower_story, "Draft date", settings.default_style), + add_lines( + lower_story, + "Contact", + settings.contact_style, + space_before=settings.line_height, + ), + add_lines( + lower_story, + "Copyright", + settings.centered_style, + space_before=settings.line_height, + ), + ) + ) if not title_story and not lower_story: return [] story = [] top_space = min( - settings.frame_height / 3.0, - settings.frame_height - lower_height - title_height + settings.frame_height / 3.0, settings.frame_height - lower_height - title_height ) if top_space > 0: story.append(Spacer(settings.frame_width, top_space)) @@ -503,7 +496,7 @@ def add_lines(story, attribute, style, space_before=0): story.append(Spacer(settings.frame_width, middle_space)) story += lower_story - story.append(NextPageTemplate('standard')) + story.append(NextPageTemplate("standard")) story.append(platypus.PageBreak()) return story @@ -524,23 +517,21 @@ def pdf_metadata(screenplay): ] return { - "title": ' '.join([str(line) for line in title_lines]) or None, - "subject": ' '.join([str(line) for line in subject_lines]) or None, - "author": ', '.join([str(line) for line in author_lines]) or None, + "title": " ".join([str(line) for line in title_lines]) or None, + "subject": " ".join([str(line) for line in subject_lines]) or None, + "author": ", ".join([str(line) for line in author_lines]) or None, "keywords": [ word.strip() for line in keywords_lines - for word in re.split('[,;/]', str(line)) + for word in re.split("[,;/]", str(line)) if word.strip() ], - "lang": ' '.join([str(line) for line in lang_lines]) or None, + "lang": " ".join([str(line) for line in lang_lines]) or None, } def to_pdf( - screenplay, output_filename, - template_constructor=DocTemplate, - settings=None + screenplay, output_filename, template_constructor=DocTemplate, settings=None ): settings = settings or create_default_settings() story = get_title_page_story(screenplay, settings) @@ -553,10 +544,11 @@ def to_pdf( add_dual_dialog(story, para, settings) elif isinstance(para, Action): add_paragraph( - story, para, + story, + para, settings.centered_action_style if para.centered - else settings.action_style + else settings.action_style, ) elif isinstance(para, Slug): add_slug(story, para, settings) diff --git a/screenplain/main.py b/screenplain/main.py index 660a993..facb5eb 100644 --- a/screenplain/main.py +++ b/screenplain/main.py @@ -10,9 +10,7 @@ from screenplain.parsers import fountain -output_formats = ( - 'fdx', 'html', 'pdf' -) +output_formats = ("fdx", "html", "pdf") description = """Convert text file to viewable screenplay. @@ -26,89 +24,77 @@ def invalid_format(parser, message): formats = " ".join(output_formats) parser.error( - f'{message}\nUse --format with one of the following formats: ' - f'{formats}' + f"{message}\nUse --format with one of the following formats: {formats}" ) def main(argv): parser = argparse.ArgumentParser( - description=description, - formatter_class=argparse.RawDescriptionHelpFormatter + description=description, formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( - 'input_file', - nargs='?', - help='Input screenplay file (default: stdin)' + "input_file", nargs="?", help="Input screenplay file (default: stdin)" ) + parser.add_argument("output_file", nargs="?", help="Output file (default: stdout)") parser.add_argument( - 'output_file', - nargs='?', - help='Output file (default: stdout)' - ) - parser.add_argument( - '-f', '--format', - dest='output_format', - metavar='FORMAT', + "-f", + "--format", + dest="output_format", + metavar="FORMAT", help=( - 'Set what kind of file to create. FORMAT can be one of ' - f'{", ".join(output_formats)}' - ) + "Set what kind of file to create. FORMAT can be one of " + f"{', '.join(output_formats)}" + ), ) parser.add_argument( - '--bare', - action='store_true', + "--bare", + action="store_true", help=( - 'For HTML output, only output the actual screenplay, ' - 'not a complete HTML document.' - ) + "For HTML output, only output the actual screenplay, " + "not a complete HTML document." + ), ) parser.add_argument( - '--css', - metavar='FILE', + "--css", + metavar="FILE", help=( - 'For HTML output, inline the given CSS file in the HTML document ' - 'instead of the default.' - ) + "For HTML output, inline the given CSS file in the HTML document " + "instead of the default." + ), ) parser.add_argument( - '--strong', - action='store_true', - help=( - 'For PDF output, scene headings will appear ' - 'Bold and Underlined.' - ) + "--strong", + action="store_true", + help=("For PDF output, scene headings will appear Bold and Underlined."), ) parser.add_argument( - '--standard-font', - action='store_true', + "--standard-font", + action="store_true", help=( - "For PDF output, " - "use the standard Courier font instead of Courier Prime." - ) + "For PDF output, use the standard Courier font instead of Courier Prime." + ), ) parser.add_argument( - '--encoding', - default='utf-8-sig', + "--encoding", + default="utf-8-sig", help="Text encoding of the input file. " - "Should be one of Python's built-in encodings." + "Should be one of Python's built-in encodings.", ) parser.add_argument( - '--encoding-errors', - default='strict', - choices=['strict', 'ignore', 'replace', - 'backslashreplace', 'surrogateescape'], - help='How to handle invalid character codes in the input file' + "--encoding-errors", + default="strict", + choices=["strict", "ignore", "replace", "backslashreplace", "surrogateescape"], + help="How to handle invalid character codes in the input file", ) args = parser.parse_args(argv) # Handle dash as stdin/stdout - if not args.input_file or args.input_file == '-': + if not args.input_file or args.input_file == "-": input_file = None else: input_file = args.input_file - if not args.output_file or args.output_file == '-': + if not args.output_file or args.output_file == "-": output_file = None else: output_file = args.output_file @@ -116,47 +102,43 @@ def main(argv): try: codecs.lookup(args.encoding) except LookupError: - parser.error(f'Unknown encoding: {args.encoding}') + parser.error(f"Unknown encoding: {args.encoding}") format = args.output_format if format is None and output_file: - if output_file.endswith('.fdx'): - format = 'fdx' - elif output_file.endswith('.html'): - format = 'html' - elif output_file.endswith('.pdf'): - format = 'pdf' + if output_file.endswith(".fdx"): + format = "fdx" + elif output_file.endswith(".html"): + format = "html" + elif output_file.endswith(".pdf"): + format = "pdf" else: invalid_format( - parser, - f'Could not detect output format from file name {output_file}' + parser, f"Could not detect output format from file name {output_file}" ) if format not in output_formats: - invalid_format( - parser, f'Unsupported output format: "{format}".' - ) + invalid_format(parser, f'Unsupported output format: "{format}".') if input_file: input = codecs.open( - input_file, 'r', - encoding=args.encoding, - errors=args.encoding_errors) + input_file, "r", encoding=args.encoding, errors=args.encoding_errors + ) else: input = codecs.getreader(args.encoding)(sys.stdin.buffer) input.errors = args.encoding_errors screenplay = fountain.parse(input) - if format == 'pdf': + if format == "pdf": output_encoding = None else: - output_encoding = 'utf-8' + output_encoding = "utf-8" if output_file: if output_encoding: - output = codecs.open(output_file, 'w', output_encoding) + output = codecs.open(output_file, "w", output_encoding) else: - output = open(output_file, 'wb') + output = open(output_file, "wb") else: if output_encoding: output = codecs.getwriter(output_encoding)(sys.stdout.buffer) @@ -164,17 +146,17 @@ def main(argv): output = sys.stdout.buffer try: - if format == 'fdx': + if format == "fdx": from screenplain.export.fdx import to_fdx + to_fdx(screenplay, output) - elif format == 'html': + elif format == "html": from screenplain.export.html import convert - convert( - screenplay, output, - css_file=args.css, bare=args.bare - ) - elif format == 'pdf': + + convert(screenplay, output, css_file=args.css, bare=args.bare) + elif format == "pdf": from screenplain.export import pdf + font_settings = None if args.standard_font: font_settings = pdf.get_standard_font_settings() @@ -194,5 +176,5 @@ def cli(): main(sys.argv[1:]) -if __name__ == '__main__': +if __name__ == "__main__": main(sys.argv[1:]) diff --git a/screenplain/parsers/fountain.py b/screenplain/parsers/fountain.py index cad9e29..6213f1c 100644 --- a/screenplain/parsers/fountain.py +++ b/screenplain/parsers/fountain.py @@ -19,28 +19,28 @@ ) slug_regexes = ( - re.compile(r'^(INT|EXT|EST)[ .]'), - re.compile(r'^(INT\.?/EXT\.?)[ .]'), - re.compile(r'^I/E[ .]'), + re.compile(r"^(INT|EXT|EST)[ .]"), + re.compile(r"^(INT\.?/EXT\.?)[ .]"), + re.compile(r"^I/E[ .]"), ) -boneyard_re = re.compile(r'/\*.*?\*/', flags=re.DOTALL) +boneyard_re = re.compile(r"/\*.*?\*/", flags=re.DOTALL) -TWOSPACE = ' ' * 2 +TWOSPACE = " " * 2 -linebreak_re = re.compile('\r\n|\n|\r') +linebreak_re = re.compile("\r\n|\n|\r") -title_page_key_re = re.compile(r'([^:]+):\s*(.*)') -title_page_value_re = re.compile(r'(?:\s{3,}|\t)(.+)') +title_page_key_re = re.compile(r"([^:]+):\s*(.*)") +title_page_value_re = re.compile(r"(?:\s{3,}|\t)(.+)") -centered_re = re.compile(r'\s*>\s*(.*?)\s*<\s*$') -dual_dialog_re = re.compile(r'^(.+?)(\s*\^)$') -slug_re = re.compile(r'(?:(\.)(?=[^.])\s*)?(\S.*?)\s*$') -scene_number_re = re.compile(r'(.*?)\s*(?:#([\w\-.]+)#)\s*$') -section_re = re.compile(r'^(#{1,6})\s*([^#].*)$') -transition_re = re.compile(r'(>?)\s*(.+?)(TO:)?$') -page_break_re = re.compile(r'^={3,}$') -note_re = re.compile(r'\[\[.*?\]\]', re.DOTALL) +centered_re = re.compile(r"\s*>\s*(.*?)\s*<\s*$") +dual_dialog_re = re.compile(r"^(.+?)(\s*\^)$") +slug_re = re.compile(r"(?:(\.)(?=[^.])\s*)?(\S.*?)\s*$") +scene_number_re = re.compile(r"(.*?)\s*(?:#([\w\-.]+)#)\s*$") +section_re = re.compile(r"^(#{1,6})\s*([^#].*)$") +transition_re = re.compile(r"(>?)\s*(.+?)(TO:)?$") +page_break_re = re.compile(r"^={3,}$") +note_re = re.compile(r"\[\[.*?\]\]", re.DOTALL) def _sequence_to_rich(lines): @@ -49,8 +49,7 @@ def _sequence_to_rich(lines): def _string_to_rich(line): - """Converts a single string into a RichString. - """ + """Converts a single string into a RichString.""" return parse_emphasis(line) @@ -63,15 +62,15 @@ def update_list(self, previous_paragraphs): Modifies the `previous_paragraphs` list. """ ( - self.append_forced_action(previous_paragraphs) or - self.append_page_break(previous_paragraphs) or - self.append_synopsis(previous_paragraphs) or - self.append_sections_and_synopsises(previous_paragraphs) or - self.append_slug(previous_paragraphs) or - self.append_centered_action(previous_paragraphs) or - self.append_dialog(previous_paragraphs) or - self.append_transition(previous_paragraphs) or - self.append_action(previous_paragraphs) + self.append_forced_action(previous_paragraphs) + or self.append_page_break(previous_paragraphs) + or self.append_synopsis(previous_paragraphs) + or self.append_sections_and_synopsises(previous_paragraphs) + or self.append_slug(previous_paragraphs) + or self.append_centered_action(previous_paragraphs) + or self.append_dialog(previous_paragraphs) + or self.append_transition(previous_paragraphs) + or self.append_action(previous_paragraphs) ) def append_slug(self, paragraphs): @@ -96,7 +95,6 @@ def append_slug(self, paragraphs): return True def append_sections_and_synopsises(self, paragraphs): - new_paragraphs = [] for line in self.lines: @@ -106,9 +104,9 @@ def append_sections_and_synopsises(self, paragraphs): section = Section(_string_to_rich(text), len(hashes)) new_paragraphs.append(section) elif ( - line.startswith('=') and - new_paragraphs and - hasattr(new_paragraphs[-1], 'set_synopsis') + line.startswith("=") + and new_paragraphs + and hasattr(new_paragraphs[-1], "set_synopsis") ): new_paragraphs[-1].set_synopsis(line[1:].lstrip()) else: @@ -120,15 +118,20 @@ def append_sections_and_synopsises(self, paragraphs): def append_centered_action(self, paragraphs): if not all(centered_re.match(line) for line in self.lines): return False - paragraphs.append(Action(_sequence_to_rich( - centered_re.match(line).group(1) for line in self.lines - ), centered=True)) + paragraphs.append( + Action( + _sequence_to_rich( + centered_re.match(line).group(1) for line in self.lines + ), + centered=True, + ) + ) return True def _create_dialog(self, character): return Dialog( parse_emphasis(character.strip()), - _sequence_to_rich(line.strip() for line in self.lines[1:]) + _sequence_to_rich(line.strip() for line in self.lines[1:]), ) def append_dialog(self, paragraphs): @@ -138,10 +141,10 @@ def append_dialog(self, paragraphs): character = self.lines[0] if character.endswith(TWOSPACE): return False - if character.startswith('@') and len(character) >= 2: + if character.startswith("@") and len(character) >= 2: character = character[1:] else: - before_paren, *_ = character.split('(', 1) + before_paren, *_ = character.split("(", 1) if not before_paren.isupper(): return False @@ -167,39 +170,39 @@ def append_transition(self, paragraphs): if greater_than: paragraphs.append( - Transition(_string_to_rich(text.upper() + (to_colon or ''))) + Transition(_string_to_rich(text.upper() + (to_colon or ""))) ) return True if text.isupper() and to_colon: - paragraphs.append( - Transition(_string_to_rich(text + to_colon)) - ) + paragraphs.append(Transition(_string_to_rich(text + to_colon))) return True return False def append_forced_action(self, paragraphs): - if self.lines[0].startswith('!'): + if self.lines[0].startswith("!"): return self.append_action(paragraphs) else: return False def append_action(self, paragraphs): paragraphs.append( - Action(_sequence_to_rich( - line[1:].rstrip() if line.startswith('!') else line.rstrip() - for line in self.lines - )) + Action( + _sequence_to_rich( + line[1:].rstrip() if line.startswith("!") else line.rstrip() + for line in self.lines + ) + ) ) return True def append_synopsis(self, paragraphs): if ( - len(self.lines) == 1 and - self.lines[0].startswith('=') and - paragraphs and - hasattr(paragraphs[-1], 'set_synopsis') + len(self.lines) == 1 + and self.lines[0].startswith("=") + and paragraphs + and hasattr(paragraphs[-1], "set_synopsis") ): paragraphs[-1].set_synopsis(self.lines[0][1:].lstrip()) return True @@ -221,11 +224,11 @@ def _preprocess_line(raw_line): 'foo ' """ - return raw_line.expandtabs(4).rstrip('\r\n') + return raw_line.expandtabs(4).rstrip("\r\n") def _is_blank(line): - return line == '' or line == ' ' + return line == "" or line == " " def parse(stream): @@ -235,7 +238,7 @@ def parse(stream): """ content = stream.read() - content = boneyard_re.sub('', content) + content = boneyard_re.sub("", content) lines = linebreak_re.split(content) del content return parse_lines(lines) @@ -249,7 +252,7 @@ def parse_lines(source): """ source = (_preprocess_line(line) for line in source) - title_page_lines = list(takewhile(lambda line: line != '', source)) + title_page_lines = list(takewhile(lambda line: line != "", source)) # parse_title_page converts the keys to consistent casing. # As of now, the only use of the dictionary is generating the title page, @@ -265,8 +268,7 @@ def parse_lines(source): # The first lines were not a title page. # Parse them as part of the screenplay body. return Screenplay( - {}, - parse_body(itertools.chain(title_page_lines, [''], source)) + {}, parse_body(itertools.chain(title_page_lines, [""], source)) ) @@ -276,10 +278,10 @@ def parse_body(source): paragraphs = [] for blank, input_lines in itertools.groupby(source, _is_blank): if not blank: - as_string = note_re.sub('', '\n'.join(input_lines)) + as_string = note_re.sub("", "\n".join(input_lines)) if _is_blank(as_string): continue - paragraph = InputParagraph(as_string.split('\n')) + paragraph = InputParagraph(as_string.split("\n")) paragraph.update_list(paragraphs) return paragraphs diff --git a/screenplain/richstring.py b/screenplain/richstring.py index 693731e..46bef79 100644 --- a/screenplain/richstring.py +++ b/screenplain/richstring.py @@ -5,7 +5,7 @@ import re from html import escape as html_escape -_magic_re = re.compile('[\ue700-\ue705]') +_magic_re = re.compile("[\ue700-\ue705]") def _escape(s): @@ -13,9 +13,9 @@ def _escape(s): and non-ascii characters with ampersand escapes. """ - encoded = html_escape(s, quote=False).encode('ascii', 'xmlcharrefreplace') + encoded = html_escape(s, quote=False).encode("ascii", "xmlcharrefreplace") # In Py3, encoded is bytes type, so convert it to a string - return encoded.decode('ascii') + return encoded.decode("ascii") class RichString: @@ -27,17 +27,17 @@ def __init__(self, *segments): def __repr__(self): if not self.segments: return "empty_string" - return ' + '.join(repr(s) for s in self.segments) + return " + ".join(repr(s) for s in self.segments) def __str__(self): - return ''.join(str(s) for s in self.segments) + return "".join(str(s) for s in self.segments) def startswith(self, string): """Checks if the first segment in this string starts with a specific string. """ - if '' == string: + if "" == string: return True if not self.segments: return False @@ -48,36 +48,30 @@ def endswith(self, string): specific string. """ - if '' == string: + if "" == string: return True if not self.segments: return False return self.segments[-1].text.endswith(string) def to_html(self): - html = ''.join(seg.to_html() for seg in self.segments) - if html.startswith(' '): - return ' ' + html[1:] + html = "".join(seg.to_html() for seg in self.segments) + if html.startswith(" "): + return " " + html[1:] else: return html def __eq__(self, other): - return ( - isinstance(other, RichString) and - self.segments == other.segments - ) + return isinstance(other, RichString) and self.segments == other.segments def __ne__(self, other): - return ( - isinstance(other, RichString) and - self.segments != other.segments - ) + return isinstance(other, RichString) and self.segments != other.segments def __add__(self, other): - if hasattr(other, 'segments'): + if hasattr(other, "segments"): return RichString(*(self.segments + other.segments)) else: - raise ValueError('Concatenating requires RichString') + raise ValueError("Concatenating requires RichString") class Segment: @@ -93,24 +87,26 @@ def __init__(self, text, styles): self.text = text def __repr__(self): - styles = '+'.join( - style.name() for style in self.get_ordered_styles() - ) or 'plain' - return f'({styles})({self.text!r})' + styles = ( + "+".join(style.name() for style in self.get_ordered_styles()) or "plain" + ) + return f"({styles})({self.text!r})" def __str__(self): return self.text def __eq__(self, other): return ( - isinstance(other, Segment) and - self.text == other.text and self.styles == other.styles + isinstance(other, Segment) + and self.text == other.text + and self.styles == other.styles ) def __ne__(self, other): return ( - not isinstance(other, Segment) or - self.text != other.text or self.styles != other.styles + not isinstance(other, Segment) + or self.text != other.text + or self.styles != other.styles ) def get_ordered_styles(self): @@ -120,23 +116,23 @@ def get_ordered_styles(self): def to_html(self): ordered_styles = self.get_ordered_styles() return ( - ''.join(style.start_html for style in ordered_styles) + - re.sub( - ' +', # at least two spaces - lambda m: ' ' * (len(m.group(0)) - 1) + ' ', + "".join(style.start_html for style in ordered_styles) + + re.sub( + " +", # at least two spaces + lambda m: " " * (len(m.group(0)) - 1) + " ", _escape(self.text), - ) + - ''.join(style.end_html for style in reversed(ordered_styles)) + ) + + "".join(style.end_html for style in reversed(ordered_styles)) ) class Style: """Abstract base class for styles""" - start_magic = '' - end_magic = '' - start_html = '' - end_html = '' + start_magic = "" + end_magic = "" + start_html = "" + end_html = "" @classmethod def name(cls): @@ -144,69 +140,67 @@ def name(cls): class Italic(Style): - parse_re = re.compile( # one star - r'\*' + r"\*" # anything but a space, then text - r'([^\s].*?)' + r"([^\s].*?)" # finishing with one star - r'\*' + r"\*" # must not be followed by star - r'(?!\*)' + r"(?!\*)" ) - start_magic = '\ue700' - end_magic = '\ue701' + start_magic = "\ue700" + end_magic = "\ue701" - start_html = '' - end_html = '' + start_html = "" + end_html = "" class Bold(Style): - parse_re = re.compile( # two stars - r'\*\*' + r"\*\*" # must not be followed by space - r'(?=\S)' + r"(?=\S)" # inside text - r'(.+?[*_]*)' + r"(.+?[*_]*)" # finishing with two stars - r'(?<=\S)\*\*' + r"(?<=\S)\*\*" ) - start_magic = '\ue702' - end_magic = '\ue703' + start_magic = "\ue702" + end_magic = "\ue703" - start_html = '' - end_html = '' + start_html = "" + end_html = "" class Underline(Style): - parse_re = re.compile( # underline - r'_' + r"_" # must not be followed by space - r'(?=\S)' + r"(?=\S)" # inside text - r'([^_]+)' + r"([^_]+)" # finishing with underline - r'(?<=\S)_' + r"(?<=\S)_" ) - start_magic = '\ue704' - end_magic = '\ue705' + start_magic = "\ue704" + end_magic = "\ue705" - start_html = '' # TODO: use an actual html5 tag - end_html = '' + start_html = "" # TODO: use an actual html5 tag + end_html = "" class _CreateStyledString: """Function object that creates a RichString object with a single segment with a specified style. """ + def __init__(self, styles): self.styles = set(styles) @@ -225,7 +219,7 @@ def __add__(self, other): empty_string = RichString() # A special unicode character to use for a literal '*' -literal_star = '\ue706' +literal_star = "\ue706" # All styles. Note: order matters! This is the order they are parsed. all_styles = (Bold, Italic, Underline) @@ -239,7 +233,7 @@ def _unescape(source): '\ue706hello\ue706' """ - return source.replace('\\*', literal_star) + return source.replace("\\*", literal_star) def _demagic_literals(text): @@ -248,7 +242,7 @@ def _demagic_literals(text): >>> _demagic_literals('\ue706hello\ue706') '*hello*' """ - return text.replace(literal_star, '*') + return text.replace(literal_star, "*") def parse_emphasis(source): @@ -268,9 +262,7 @@ def parse_emphasis(source): source = _unescape(source) for style in all_styles: - source = style.parse_re.sub( - style.start_magic + r'\1' + style.end_magic, source - ) + source = style.parse_re.sub(style.start_magic + r"\1" + style.end_magic, source) # Convert magic characters back, so they are printable again. source = _demagic_literals(source) diff --git a/screenplain/types.py b/screenplain/types.py index 2a3b66f..0ca676b 100644 --- a/screenplain/types.py +++ b/screenplain/types.py @@ -48,7 +48,6 @@ def __iter__(self): class Slug: - def __init__(self, line, scene_number=None): """Creates a scene heading (slug). The line parameter is a RichString with the slugline. @@ -79,13 +78,13 @@ def set_synopsis(self, text): self.synopsis = text def __repr__(self): - return f'Section({self.text!r}, {self.level!r}, {self.synopsis!r})' + return f"Section({self.text!r}, {self.level!r}, {self.synopsis!r})" def __eq__(self, other): return ( - self.text == other.text and - self.level == other.level and - self.synopsis == other.synopsis + self.text == other.text + and self.level == other.level + and self.synopsis == other.synopsis ) @@ -99,14 +98,14 @@ def __init__(self, character, lines=None): def _parse(self, lines): inside_parenthesis = False for line in lines: - if line.startswith('('): + if line.startswith("("): inside_parenthesis = True self.blocks.append((inside_parenthesis, line)) - if line.endswith(')'): + if line.endswith(")"): inside_parenthesis = False def add_line(self, line): - parenthetical = line.startswith('(') + parenthetical = line.startswith("(") self.blocks.append((parenthetical, line)) diff --git a/setup.py b/setup.py index c0ea021..48c6a6e 100755 --- a/setup.py +++ b/setup.py @@ -1,46 +1,39 @@ #!/usr/bin/env python -from setuptools import setup - # read the contents of your README file from os import path + +from setuptools import setup + this_directory = path.abspath(path.dirname(__file__)) -with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: +with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: long_description = f.read() setup( - name='screenplain', - version='0.12.0', - description='Convert text file to viewable screenplay.', + name="screenplain", + version="0.12.0", + description="Convert text file to viewable screenplay.", long_description=long_description, - long_description_content_type='text/markdown', - author='Martin Vilcans', - author_email='screenplain@librador.com', - url='http://www.screenplain.com/', + long_description_content_type="text/markdown", + author="Martin Vilcans", + author_email="screenplain@librador.com", + url="http://www.screenplain.com/", project_urls={ - 'Web Page': 'http://www.screenplain.com/', - 'Source': 'https://github.com/vilcans/screenplain', + "Web Page": "http://www.screenplain.com/", + "Source": "https://github.com/vilcans/screenplain", }, - license='MIT', - install_requires=[ - 'reportlab' - ], + license="MIT", + install_requires=["reportlab"], packages=[ - 'screenplain', - 'screenplain.export', - 'screenplain.parsers', + "screenplain", + "screenplain.export", + "screenplain.parsers", ], - package_data={ - 'screenplain.export': ['default.css', 'courier_prime/**'] - }, - entry_points={ - 'console_scripts': [ - 'screenplain = screenplain.main:cli' - ] - }, + package_data={"screenplain.export": ["default.css", "courier_prime/**"]}, + entry_points={"console_scripts": ["screenplain = screenplain.main:cli"]}, classifiers=[ - 'Programming Language :: Python :: 3', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", ], ) diff --git a/tests/fdx_test.py b/tests/fdx_test.py index c591839..204ca12 100644 --- a/tests/fdx_test.py +++ b/tests/fdx_test.py @@ -10,50 +10,43 @@ class OutputTests(TestCase): - def setUp(self): self.out = StringIO() def test_plain_text_should_have_no_style(self): - write_text(self.out, plain('hello'), False) - self.assertEqual( - self.out.getvalue(), - ' hello\n' - ) + write_text(self.out, plain("hello"), False) + self.assertEqual(self.out.getvalue(), " hello\n") def test_bold_text_should_have_bold_style(self): - write_text(self.out, bold('hello'), False) - self.assertEqual( - self.out.getvalue(), - ' hello\n' - ) + write_text(self.out, bold("hello"), False) + self.assertEqual(self.out.getvalue(), ' hello\n') def test_sequential_styles(self): - rich = plain('plain') + bold('b') + italic('i') + rich = plain("plain") + bold("b") + italic("i") write_text(self.out, rich, False) self.assertEqual( self.out.getvalue(), - ' plain\n' + " plain\n" ' b\n' - ' i\n' + ' i\n', ) def test_several_styles(self): - rich = bold('outer') + (bold + italic)('inner') + bold('outer') + rich = bold("outer") + (bold + italic)("inner") + bold("outer") write_text(self.out, rich, False) self.assertEqual( self.out.getvalue(), ' outer\n' ' inner\n' - ' outer\n' + ' outer\n', ) def test_write_text_adds_line_break_if_requested(self): - rich = bold('outer') + (bold + italic)('inner') + bold('outer') + rich = bold("outer") + (bold + italic)("inner") + bold("outer") write_text(self.out, rich, True) self.assertEqual( self.out.getvalue(), ' outer\n' ' inner\n' - ' outer\n\n' # note newline + ' outer\n\n', # note newline ) diff --git a/tests/files/dialogue-with-blank-line.fountain b/tests/files/dialogue-with-blank-line.fountain index a64a9f7..97da070 100644 --- a/tests/files/dialogue-with-blank-line.fountain +++ b/tests/files/dialogue-with-blank-line.fountain @@ -1,4 +1,4 @@ DEALER Dealer gets a seven. - + Hit or stand sir? diff --git a/tests/files/notes.fountain b/tests/files/notes.fountain index f72b4fc..317752f 100644 --- a/tests/files/notes.fountain +++ b/tests/files/notes.fountain @@ -6,7 +6,7 @@ here]]After2 Before3 [[multiline note - + with a blank line]]After3 Multiple [[notes]]on one[[line]] diff --git a/tests/files_test.py b/tests/files_test.py index 0162af6..98f4148 100644 --- a/tests/files_test.py +++ b/tests/files_test.py @@ -11,10 +11,10 @@ from screenplain.main import main -source_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'files')) +source_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "files")) -line_break_re = re.compile('\\s*\n\\s*') -spaces_re = re.compile('[ \t]+') +line_break_re = re.compile("\\s*\n\\s*") +spaces_re = re.compile("[ \t]+") def read_file(path): @@ -31,12 +31,11 @@ def clean_string(s): 'foo bar' """ - return spaces_re.sub(' ', line_break_re.sub('\n', s)) + return spaces_re.sub(" ", line_break_re.sub("\n", s)) class FileTests(TestCase): - """High level tests that runs Screenplain using command line arguments. - """ + """High level tests that runs Screenplain using command line arguments.""" maxDiff = None @@ -52,11 +51,7 @@ def source(self, filename): def target(self, name): return os.path.join(self.dir, name) - def convert( - self, - input_file, output_file, expected_results_file, - *options - ): + def convert(self, input_file, output_file, expected_results_file, *options): input_path = self.source(input_file) output_path = self.target(output_file) main(list(options) + [input_path, output_path]) @@ -75,18 +70,11 @@ def add_file_case(cls, source_file, expected_results_file): def test_function(self): actual, expected = self.convert( - source_file, base + '.' + extension, - expected_results_file, - '--bare' + source_file, base + "." + extension, expected_results_file, "--bare" ) self.assertMultiLineEqual(expected, actual) - func_name = ( - 'test_' + - source_file.replace('.', '_') + - '_to_' + - extension - ) + func_name = "test_" + source_file.replace(".", "_") + "_to_" + extension setattr(cls, func_name, test_function) @@ -104,17 +92,14 @@ def _create_tests(): as they are handled by visual/pdf_test.py. """ test_files = os.listdir(source_dir) - source_files = [f for f in test_files if f.endswith('.fountain')] + source_files = [f for f in test_files if f.endswith(".fountain")] expect_files = [ - f for f in test_files - if not f.endswith('.fountain') and not f.endswith('.pdf') + f for f in test_files if not f.endswith(".fountain") and not f.endswith(".pdf") ] for source in source_files: - base = os.path.splitext(source)[0] + '.' - for expected in ( - f for f in expect_files if f.startswith(base) - ): + base = os.path.splitext(source)[0] + "." + for expected in (f for f in expect_files if f.startswith(base)): FileTests.add_file_case(source, expected) diff --git a/tests/fountain_test.py b/tests/fountain_test.py index d1d6a87..7569c30 100644 --- a/tests/fountain_test.py +++ b/tests/fountain_test.py @@ -19,396 +19,436 @@ def parse(lines): - content = '\n'.join(lines) + content = "\n".join(lines) return list(fountain.parse(StringIO(content))) class SlugTests(TestCase): def test_slug_with_prefix(self): - paras = parse([ - 'INT. SOMEWHERE - DAY', - '', - 'THIS IS JUST ACTION', - ]) + paras = parse( + [ + "INT. SOMEWHERE - DAY", + "", + "THIS IS JUST ACTION", + ] + ) self.assertEqual([Slug, Action], [type(p) for p in paras]) def test_slug_must_be_single_line(self): - paras = parse([ - 'INT. SOMEWHERE - DAY', - 'ANOTHER LINE', - '', - 'Some action', - ]) + paras = parse( + [ + "INT. SOMEWHERE - DAY", + "ANOTHER LINE", + "", + "Some action", + ] + ) self.assertEqual([Dialog, Action], [type(p) for p in paras]) # What looks like a scene headingis parsed as a character name. # Unexpected perhaps, but that's how I interpreted the spec. - self.assertEqual(plain('INT. SOMEWHERE - DAY'), paras[0].character) - self.assertEqual([plain('Some action')], paras[1].lines) + self.assertEqual(plain("INT. SOMEWHERE - DAY"), paras[0].character) + self.assertEqual([plain("Some action")], paras[1].lines) def test_action_is_not_a_slug(self): - paras = parse([ - '', - 'THIS IS JUST ACTION', - ]) + paras = parse( + [ + "", + "THIS IS JUST ACTION", + ] + ) self.assertEqual([Action], [type(p) for p in paras]) def test_two_lines_creates_no_slug(self): - types = [type(p) for p in parse([ - '', - '', - 'This is a slug', - '', - ])] + types = [ + type(p) + for p in parse( + [ + "", + "", + "This is a slug", + "", + ] + ) + ] # This used to be Slug. Changed in the Jan 2012 version of the spec. self.assertEqual([Action], types) def test_period_creates_slug(self): - paras = parse([ - '.SNIPER SCOPE POV', - '', - ]) + paras = parse( + [ + ".SNIPER SCOPE POV", + "", + ] + ) self.assertEqual(1, len(paras)) self.assertEqual(Slug, type(paras[0])) - self.assertEqual(plain('SNIPER SCOPE POV'), paras[0].line) + self.assertEqual(plain("SNIPER SCOPE POV"), paras[0].line) def test_more_than_one_period_does_not_create_slug(self): - paras = parse([ - '..AND THEN...', - '', - ]) + paras = parse( + [ + "..AND THEN...", + "", + ] + ) self.assertEqual(1, len(paras)) self.assertEqual(Action, type(paras[0])) - self.assertEqual(plain('..AND THEN...'), paras[0].lines[0]) + self.assertEqual(plain("..AND THEN..."), paras[0].lines[0]) def test_scene_number_is_parsed(self): - paras = parse(['EXT SOMEWHERE - DAY #42#']) - self.assertEqual(plain('EXT SOMEWHERE - DAY'), paras[0].line) - self.assertEqual(plain('42'), paras[0].scene_number) + paras = parse(["EXT SOMEWHERE - DAY #42#"]) + self.assertEqual(plain("EXT SOMEWHERE - DAY"), paras[0].line) + self.assertEqual(plain("42"), paras[0].scene_number) def test_only_last_two_hashes_in_slug_used_for_scene_number(self): - paras = parse(['INT ROOM #237 #42#']) - self.assertEqual(plain('42'), paras[0].scene_number) - self.assertEqual(plain('INT ROOM #237'), paras[0].line) + paras = parse(["INT ROOM #237 #42#"]) + self.assertEqual(plain("42"), paras[0].scene_number) + self.assertEqual(plain("INT ROOM #237"), paras[0].line) def test_scene_number_must_be_alphanumeric(self): - paras = parse(['.SOMEWHERE #*HELLO*#']) + paras = parse([".SOMEWHERE #*HELLO*#"]) self.assertIsNone(paras[0].scene_number) self.assertEqual( - (plain)('SOMEWHERE #') + (italic)('HELLO') + (plain)('#'), - paras[0].line + (plain)("SOMEWHERE #") + (italic)("HELLO") + (plain)("#"), paras[0].line ) class SectionTests(TestCase): def test_section_parsed_correctly(self): - paras = parse([ - '# first level', - '', - '## second level', - ]) + paras = parse( + [ + "# first level", + "", + "## second level", + ] + ) self.assertEqual([Section, Section], [type(p) for p in paras]) self.assertEqual(1, paras[0].level) - self.assertEqual(plain('first level'), paras[0].text) + self.assertEqual(plain("first level"), paras[0].text) self.assertEqual(2, paras[1].level) - self.assertEqual(plain('second level'), paras[1].text) + self.assertEqual(plain("second level"), paras[1].text) def test_multiple_sections_in_one_paragraph(self): - paras = parse([ - '# first level', - '## second level', - '# first level again' - ]) - self.assertEqual( - [Section, Section, Section], - [type(p) for p in paras] - ) + paras = parse(["# first level", "## second level", "# first level again"]) + self.assertEqual([Section, Section, Section], [type(p) for p in paras]) self.assertEqual(1, paras[0].level) - self.assertEqual(plain('first level'), paras[0].text) + self.assertEqual(plain("first level"), paras[0].text) self.assertEqual(2, paras[1].level) - self.assertEqual(plain('second level'), paras[1].text) + self.assertEqual(plain("second level"), paras[1].text) self.assertEqual(1, paras[2].level) - self.assertEqual(plain('first level again'), paras[2].text) + self.assertEqual(plain("first level again"), paras[2].text) def test_multiple_sections_with_synopsis(self): - paras = parse([ - '# first level', - '= level one synopsis', - '## second level', - ]) - self.assertEqual([ - Section(plain('first level'), 1, 'level one synopsis'), - Section(plain('second level'), 2, None), - ], paras) + paras = parse( + [ + "# first level", + "= level one synopsis", + "## second level", + ] + ) + self.assertEqual( + [ + Section(plain("first level"), 1, "level one synopsis"), + Section(plain("second level"), 2, None), + ], + paras, + ) class DialogTests(TestCase): # A Character element is any line entirely in caps, with one empty # line before it and without an empty line after it. def test_all_caps_is_character(self): - paras = [p for p in parse([ - 'SOME GUY', - 'Hello', - ])] + paras = [ + p + for p in parse( + [ + "SOME GUY", + "Hello", + ] + ) + ] self.assertEqual(1, len(paras)) dialog = paras[0] self.assertEqual(Dialog, type(dialog)) - self.assertEqual(plain('SOME GUY'), dialog.character) + self.assertEqual(plain("SOME GUY"), dialog.character) def test_alphanumeric_character(self): - paras = parse([ - 'R2D2', - 'Bee-bop', - ]) + paras = parse( + [ + "R2D2", + "Bee-bop", + ] + ) self.assertEqual([Dialog], [type(p) for p in paras]) - self.assertEqual(plain('R2D2'), paras[0].character) + self.assertEqual(plain("R2D2"), paras[0].character) # Spec http://fountain.io/syntax#section-character: # Character names must include at least one alphabetical character. # "R2D2" works, but "23" does not. def test_nonalpha_character(self): - paras = parse([ - '23', - 'Hello', - ]) + paras = parse( + [ + "23", + "Hello", + ] + ) self.assertEqual([Action], [type(p) for p in paras]) # Spec http://fountain.io/syntax#section-character: # You can force a Character element by preceding it with the "at" symbol @. def test_at_sign_forces_dialog(self): - paras = parse([ - '@McCLANE', - 'Yippee ki-yay', - ]) + paras = parse( + [ + "@McCLANE", + "Yippee ki-yay", + ] + ) self.assertEqual([Dialog], [type(p) for p in paras]) - self.assertEqual(plain('McCLANE'), paras[0].character) + self.assertEqual(plain("McCLANE"), paras[0].character) def test_twospaced_line_is_not_character(self): - paras = parse([ - 'SCANNING THE AISLES... ', - 'Where is that pit boss?', - ]) + paras = parse( + [ + "SCANNING THE AISLES... ", + "Where is that pit boss?", + ] + ) self.assertEqual([Action], [type(p) for p in paras]) def test_simple_parenthetical(self): - paras = parse([ - 'STEEL', - '(starting the engine)', - 'So much for retirement!', - ]) + paras = parse( + [ + "STEEL", + "(starting the engine)", + "So much for retirement!", + ] + ) self.assertEqual(1, len(paras)) dialog = paras[0] self.assertEqual(2, len(dialog.blocks)) - self.assertEqual( - (True, plain('(starting the engine)')), - dialog.blocks[0] - ) - self.assertEqual( - (False, plain('So much for retirement!')), - dialog.blocks[1] - ) + self.assertEqual((True, plain("(starting the engine)")), dialog.blocks[0]) + self.assertEqual((False, plain("So much for retirement!")), dialog.blocks[1]) def test_twospace_keeps_dialog_together(self): - paras = parse([ - 'SOMEONE', - 'One', - ' ', - 'Two', - ]) + paras = parse( + [ + "SOMEONE", + "One", + " ", + "Two", + ] + ) self.assertEqual([Dialog], [type(p) for p in paras]) - self.assertEqual([ - (False, plain('One')), - (False, empty_string), - (False, plain('Two')), - ], paras[0].blocks) + self.assertEqual( + [ + (False, plain("One")), + (False, empty_string), + (False, plain("Two")), + ], + paras[0].blocks, + ) def test_dual_dialog(self): - paras = parse([ - 'BRICK', - 'Fuck retirement.', - '', - 'STEEL ^', - 'Fuck retirement!', - ]) + paras = parse( + [ + "BRICK", + "Fuck retirement.", + "", + "STEEL ^", + "Fuck retirement!", + ] + ) self.assertEqual([DualDialog], [type(p) for p in paras]) dual = paras[0] - self.assertEqual(plain('BRICK'), dual.left.character) - self.assertEqual( - [(False, plain('Fuck retirement.'))], - dual.left.blocks - ) - self.assertEqual(plain('STEEL'), dual.right.character) - self.assertEqual( - [(False, plain('Fuck retirement!'))], - dual.right.blocks - ) + self.assertEqual(plain("BRICK"), dual.left.character) + self.assertEqual([(False, plain("Fuck retirement."))], dual.left.blocks) + self.assertEqual(plain("STEEL"), dual.right.character) + self.assertEqual([(False, plain("Fuck retirement!"))], dual.right.blocks) def test_dual_dialog_without_previous_dialog_is_ignored(self): - paras = parse([ - 'Brick strolls down the street.', - '', - 'BRICK ^', - 'Nice retirement.', - ]) + paras = parse( + [ + "Brick strolls down the street.", + "", + "BRICK ^", + "Nice retirement.", + ] + ) self.assertEqual([Action, Dialog], [type(p) for p in paras]) dialog = paras[1] - self.assertEqual(plain('BRICK ^'), dialog.character) - self.assertEqual([ - (False, plain('Nice retirement.')) - ], dialog.blocks) + self.assertEqual(plain("BRICK ^"), dialog.character) + self.assertEqual([(False, plain("Nice retirement."))], dialog.blocks) def test_leading_and_trailing_spaces_in_dialog(self): - paras = parse([ - 'JULIET', - 'O Romeo, Romeo! wherefore art thou Romeo?', - ' Deny thy father and refuse thy name; ', - 'Or, if thou wilt not, be but sworn my love,', - " And I'll no longer be a Capulet.", - ]) + paras = parse( + [ + "JULIET", + "O Romeo, Romeo! wherefore art thou Romeo?", + " Deny thy father and refuse thy name; ", + "Or, if thou wilt not, be but sworn my love,", + " And I'll no longer be a Capulet.", + ] + ) self.assertEqual([Dialog], [type(p) for p in paras]) - self.assertEqual([ - (False, plain('O Romeo, Romeo! wherefore art thou Romeo?')), - (False, plain('Deny thy father and refuse thy name;')), - (False, plain('Or, if thou wilt not, be but sworn my love,')), - (False, plain("And I'll no longer be a Capulet.")), - ], paras[0].blocks) + self.assertEqual( + [ + (False, plain("O Romeo, Romeo! wherefore art thou Romeo?")), + (False, plain("Deny thy father and refuse thy name;")), + (False, plain("Or, if thou wilt not, be but sworn my love,")), + (False, plain("And I'll no longer be a Capulet.")), + ], + paras[0].blocks, + ) class TransitionTests(TestCase): - def test_standard_transition(self): - paras = parse([ - 'Jack begins to argue vociferously in Vietnamese (?)', - '', - 'CUT TO:', - '', - "EXT. BRICK'S POOL - DAY", - ]) + paras = parse( + [ + "Jack begins to argue vociferously in Vietnamese (?)", + "", + "CUT TO:", + "", + "EXT. BRICK'S POOL - DAY", + ] + ) self.assertEqual([Action, Transition, Slug], [type(p) for p in paras]) def test_transition_must_end_with_to(self): - paras = parse([ - 'CUT TOO:', - '', - "EXT. BRICK'S POOL - DAY", - ]) + paras = parse( + [ + "CUT TOO:", + "", + "EXT. BRICK'S POOL - DAY", + ] + ) self.assertEqual([Action, Slug], [type(p) for p in paras]) def test_transition_needs_to_be_upper_case(self): - paras = parse([ - 'Jack begins to argue vociferously in Vietnamese (?)', - '', - 'cut to:', - '', - "EXT. BRICK'S POOL - DAY", - ]) + paras = parse( + [ + "Jack begins to argue vociferously in Vietnamese (?)", + "", + "cut to:", + "", + "EXT. BRICK'S POOL - DAY", + ] + ) self.assertEqual([Action, Action, Slug], [type(p) for p in paras]) def test_not_a_transition_on_trailing_whitespace(self): - paras = parse([ - 'Jack begins to argue vociferously in Vietnamese (?)', - '', - 'CUT TO: ', - '', - "EXT. BRICK'S POOL - DAY", - ]) + paras = parse( + [ + "Jack begins to argue vociferously in Vietnamese (?)", + "", + "CUT TO: ", + "", + "EXT. BRICK'S POOL - DAY", + ] + ) self.assertEqual([Action, Action, Slug], [type(p) for p in paras]) def test_transition_does_not_have_to_be_followed_by_slug(self): # The "followed by slug" requirement is gone from the Jan 2012 spec - paras = parse([ - 'Bill lights a cigarette.', - '', - 'CUT TO:', - '', - 'SOME GUY mowing the lawn.', - ]) - self.assertEqual( - [Action, Transition, Action], - [type(p) for p in paras] + paras = parse( + [ + "Bill lights a cigarette.", + "", + "CUT TO:", + "", + "SOME GUY mowing the lawn.", + ] ) + self.assertEqual([Action, Transition, Action], [type(p) for p in paras]) def test_greater_than_sign_means_transition(self): - paras = parse([ - 'Bill blows out the match.', - '', - '> FADE OUT.', - '', - '.DARKNESS', - ]) + paras = parse( + [ + "Bill blows out the match.", + "", + "> FADE OUT.", + "", + ".DARKNESS", + ] + ) self.assertEqual([Action, Transition, Slug], [type(p) for p in paras]) - self.assertEqual(plain('FADE OUT.'), paras[1].line) + self.assertEqual(plain("FADE OUT."), paras[1].line) def test_centered_text_is_not_parsed_as_transition(self): - paras = parse([ - 'Bill blows out the match.', - '', - '> THE END. <', - '', - 'bye!' - ]) + paras = parse(["Bill blows out the match.", "", "> THE END. <", "", "bye!"]) self.assertEqual([Action, Action, Action], [type(p) for p in paras]) def test_transition_at_end(self): - paras = parse([ - 'They stroll hand in hand down the street.', - '', - '> FADE OUT.', - ]) + paras = parse( + [ + "They stroll hand in hand down the street.", + "", + "> FADE OUT.", + ] + ) self.assertEqual([Action, Transition], [type(p) for p in paras]) - self.assertEqual(plain('FADE OUT.'), paras[1].line) + self.assertEqual(plain("FADE OUT."), paras[1].line) class ActionTests(TestCase): - def test_action_preserves_leading_whitespace(self): - paras = parse([ - 'hello', - '', - ' two spaces', - ' three spaces ', - ]) + paras = parse( + [ + "hello", + "", + " two spaces", + " three spaces ", + ] + ) self.assertEqual([Action, Action], [type(p) for p in paras]) self.assertEqual( [ - plain(' two spaces'), - plain(' three spaces'), - ], paras[1].lines + plain(" two spaces"), + plain(" three spaces"), + ], + paras[1].lines, ) def test_single_centered_line(self): - paras = parse(['> center me! <']) + paras = parse(["> center me! <"]) self.assertEqual([Action], [type(p) for p in paras]) self.assertTrue(paras[0].centered) def test_full_centered_paragraph(self): lines = [ - '> first! <', - ' > second! <', - '> third!< ', + "> first! <", + " > second! <", + "> third!< ", ] paras = parse(lines) self.assertEqual([Action], [type(p) for p in paras]) self.assertTrue(paras[0].centered) - self.assertEqual([ - plain('first!'), - plain('second!'), - plain('third!'), - ], paras[0].lines) + self.assertEqual( + [ + plain("first!"), + plain("second!"), + plain("third!"), + ], + paras[0].lines, + ) def test_upper_case_centered_not_parsed_as_dialog(self): - paras = parse([ - '> FIRST! <', - ' > SECOND! <', - '> THIRD! <', - ]) + paras = parse( + [ + "> FIRST! <", + " > SECOND! <", + "> THIRD! <", + ] + ) self.assertEqual([Action], [type(p) for p in paras]) self.assertTrue(paras[0].centered) def test_centering_marks_in_middle_of_paragraphs_are_verbatim(self): lines = [ - 'first!', - '> second! <', - 'third!', + "first!", + "> second! <", + "third!", ] paras = parse(lines) self.assertEqual([Action], [type(p) for p in paras]) @@ -418,76 +458,58 @@ def test_centering_marks_in_middle_of_paragraphs_are_verbatim(self): class SynopsisTests(TestCase): def test_synopsis_after_slug_adds_synopsis_to_scene(self): - paras = parse([ - "EXT. BRICK'S PATIO - DAY", - '', - "= Set up Brick & Steel's new life." - '', - ]) - self.assertEqual([Slug], [type(p) for p in paras]) - self.assertEqual( - "Set up Brick & Steel's new life.", - paras[0].synopsis + paras = parse( + [ + "EXT. BRICK'S PATIO - DAY", + "", + "= Set up Brick & Steel's new life.", + ] ) + self.assertEqual([Slug], [type(p) for p in paras]) + self.assertEqual("Set up Brick & Steel's new life.", paras[0].synopsis) def test_synopsis_in_section(self): - paras = parse([ - '# section one', - '', - '= In which we get to know our characters' - ]) + paras = parse(["# section one", "", "= In which we get to know our characters"]) self.assertEqual([Section], [type(p) for p in paras]) - self.assertEqual( - 'In which we get to know our characters', - paras[0].synopsis - ) + self.assertEqual("In which we get to know our characters", paras[0].synopsis) def test_synopsis_syntax_parsed_as_literal(self): - paras = parse([ - 'Some action', - '', - '= A line that just happens to look like a synopsis' - ]) + paras = parse( + ["Some action", "", "= A line that just happens to look like a synopsis"] + ) self.assertEqual([Action, Action], [type(p) for p in paras]) self.assertEqual( - [plain('= A line that just happens to look like a synopsis')], - paras[1].lines + [plain("= A line that just happens to look like a synopsis")], + paras[1].lines, ) class TitlePageTests(TestCase): - def test_basic_title_page(self): lines = [ - 'Title:', - ' _**BRICK & STEEL**_', - ' _**FULL RETIRED**_', - 'Author: Stu Maschwitz', + "Title:", + " _**BRICK & STEEL**_", + " _**FULL RETIRED**_", + "Author: Stu Maschwitz", ] self.assertDictEqual( { - 'Title': ['_**BRICK & STEEL**_', '_**FULL RETIRED**_'], - 'Author': ['Stu Maschwitz'], + "Title": ["_**BRICK & STEEL**_", "_**FULL RETIRED**_"], + "Author": ["Stu Maschwitz"], }, - fountain.parse_title_page(lines) + fountain.parse_title_page(lines), ) def test_multiple_values(self): lines = [ - 'Title: Death', - 'Title: - a love story', - 'Title:', - ' (which happens to be true)', + "Title: Death", + "Title: - a love story", + "Title:", + " (which happens to be true)", ] self.assertDictEqual( - { - 'Title': [ - 'Death', - '- a love story', - '(which happens to be true)' - ] - }, - fountain.parse_title_page(lines) + {"Title": ["Death", "- a love story", "(which happens to be true)"]}, + fountain.parse_title_page(lines), ) def test_key_casing(self): @@ -501,47 +523,42 @@ def test_key_casing(self): "Author": ["bruce"], "Draft date": ["1/10/2026"], }, - fountain.parse_title_page(lines) + fountain.parse_title_page(lines), ) def test_multiple_values_with_different_case(self): lines = [ - 'Title: Death', - 'title: - a love story', + "Title: Death", + "title: - a love story", ] self.assertDictEqual( { - 'Title': [ - 'Death', - '- a love story', + "Title": [ + "Death", + "- a love story", ] }, - fountain.parse_title_page(lines) + fountain.parse_title_page(lines), ) def test_empty_value_ignored(self): lines = [ - 'Title:', - 'Author: John August', + "Title:", + "Author: John August", ] self.assertDictEqual( - {'Author': ['John August']}, - fountain.parse_title_page(lines) + {"Author": ["John August"]}, fountain.parse_title_page(lines) ) def test_unparsable_title_page_returns_none(self): lines = [ - 'Title: Inception', - ' additional line', + "Title: Inception", + " additional line", ] self.assertIsNone(fountain.parse_title_page(lines)) class PageBreakTests(TestCase): def test_page_break_is_parsed(self): - paras = parse([ - '====', - '', - 'So here we go' - ]) + paras = parse(["====", "", "So here we go"]) self.assertEqual([PageBreak, Action], [type(p) for p in paras]) diff --git a/tests/richstring_test.py b/tests/richstring_test.py index 4f7d361..dc6752b 100644 --- a/tests/richstring_test.py +++ b/tests/richstring_test.py @@ -19,196 +19,167 @@ class LowLevelRichStringTests(TestCase): - def test_plain_string_has_one_single_segment(self): - s = plain('hello') + s = plain("hello") self.assertEqual(1, len(s.segments)) - self.assertEqual('hello', s.segments[0].text) + self.assertEqual("hello", s.segments[0].text) self.assertEqual(set(), s.segments[0].styles) class RichStringOperatorTests(TestCase): - def test_repr(self): - s = bold('Hello') + plain(' there ') + bold('folks') + s = bold("Hello") + plain(" there ") + bold("folks") self.assertEqual( - "(bold)('Hello') + (plain)(' there ') + (bold)('folks')", - repr(s) + "(bold)('Hello') + (plain)(' there ') + (bold)('folks')", repr(s) ) def test_repr_on_empty_string(self): - self.assertEqual('empty_string', repr(empty_string)) + self.assertEqual("empty_string", repr(empty_string)) def test_unicode(self): - s = bold('Hello') + plain(' there ') + bold('folks') - self.assertEqual( - 'Hello there folks', - str(s) - ) + s = bold("Hello") + plain(" there ") + bold("folks") + self.assertEqual("Hello there folks", str(s)) def test_eq(self): - self.assertEqual(bold('Hello'), bold('Hello')) - self.assertNotEqual(bold('Hello'), bold('Foo')) - self.assertNotEqual(plain('Hello'), bold('Hello')) + self.assertEqual(bold("Hello"), bold("Hello")) + self.assertNotEqual(bold("Hello"), bold("Foo")) + self.assertNotEqual(plain("Hello"), bold("Hello")) def test_ne(self): - self.assertFalse(bold('Hello') != bold('Hello')) + self.assertFalse(bold("Hello") != bold("Hello")) def test_concatenating_two_richstrings(self): - expected = RichString(Segment('hello', ()), Segment(' there', (Bold,))) - s1 = plain('hello') - s2 = bold(' there') + expected = RichString(Segment("hello", ()), Segment(" there", (Bold,))) + s1 = plain("hello") + s2 = bold(" there") result = s1 + s2 self.assertEqual(expected, result) class StyleGeneratorTests(TestCase): - def test_bold_function_creates_bold_richstring(self): - self.assertEqual( - RichString(Segment('a', (Bold,))), - bold('a') - ) + self.assertEqual(RichString(Segment("a", (Bold,))), bold("a")) def test_adding_functions(self): - self.assertEqual( - RichString(Segment('a', (Bold, Italic))), - (bold + italic)('a') - ) + self.assertEqual(RichString(Segment("a", (Bold, Italic))), (bold + italic)("a")) class RichStringTests(TestCase): - def test_plain_to_html(self): - self.assertEqual('hello', plain('hello').to_html()) + self.assertEqual("hello", plain("hello").to_html()) def test_to_html(self): s = ( - bold('bold') + plain(' normal ') + - italic('italic') + underline('wonderline') + bold("bold") + + plain(" normal ") + + italic("italic") + + underline("wonderline") ) self.assertEqual( - 'bold normal italicwonderline', - s.to_html() + "bold normal italicwonderline", s.to_html() ) class ParseEmphasisTests(TestCase): - def test_parse_without_emphasis(self): self.assertEqual( - plain('Hello'), parse_emphasis('Hello'), - 'Expected parse_emphasis to return a plain string') + plain("Hello"), + parse_emphasis("Hello"), + "Expected parse_emphasis to return a plain string", + ) def test_parse_bold(self): - self.assertEqual( - parse_emphasis('**Hello**'), - bold('Hello') - ) + self.assertEqual(parse_emphasis("**Hello**"), bold("Hello")) def test_parse_pre_and_postfix_and_bold(self): self.assertEqual( - parse_emphasis('pre**Hello**post'), - plain('pre') + bold('Hello') + plain('post') + parse_emphasis("pre**Hello**post"), + plain("pre") + bold("Hello") + plain("post"), ) def test_parse_multiple_bold(self): self.assertEqual( - parse_emphasis('x**Hello** **there**'), - plain('x') + bold('Hello') + plain(' ') + bold('there') + parse_emphasis("x**Hello** **there**"), + plain("x") + bold("Hello") + plain(" ") + bold("there"), ) def test_parse_adjacent_bold(self): - self.assertEqual( - parse_emphasis('**123**456**'), - bold('123') + plain('456**') - ) + self.assertEqual(parse_emphasis("**123**456**"), bold("123") + plain("456**")) def test_italic(self): - self.assertEqual( - parse_emphasis('*Italian style*'), - italic('Italian style') - ) + self.assertEqual(parse_emphasis("*Italian style*"), italic("Italian style")) def test_bold_inside_italic(self): self.assertEqual( - parse_emphasis('*Swedish **style** rules*'), - italic('Swedish ') + (bold + italic)('style') + italic(' rules') + parse_emphasis("*Swedish **style** rules*"), + italic("Swedish ") + (bold + italic)("style") + italic(" rules"), ) def test_italic_inside_bold(self): self.assertEqual( - parse_emphasis('**Swedish *style* rules**'), - bold('Swedish ') + (bold + italic)('style') + bold(' rules') + parse_emphasis("**Swedish *style* rules**"), + bold("Swedish ") + (bold + italic)("style") + bold(" rules"), ) def test_italic_and_bold(self): self.assertEqual( - parse_emphasis('***really strong***'), - (bold + italic)('really strong') + parse_emphasis("***really strong***"), (bold + italic)("really strong") ) def test_additional_star(self): self.assertEqual( - parse_emphasis('*foo* bar* baz'), - italic('foo') + plain(' bar* baz') + parse_emphasis("*foo* bar* baz"), italic("foo") + plain(" bar* baz") ) def test_underline(self): - self.assertEqual( - parse_emphasis('_hello_'), - underline('hello') - ) + self.assertEqual(parse_emphasis("_hello_"), underline("hello")) def test_bold_inside_underline(self): - self.assertEqual( - parse_emphasis('_**hello**_'), - (bold + underline)('hello') - ) + self.assertEqual(parse_emphasis("_**hello**_"), (bold + underline)("hello")) def test_overlapping_underscore_and_italic(self): # It's unclear what result to expect in this case. # This is one way of interpreting it self.assertEqual( - parse_emphasis('_*he_llo*'), - (italic + underline)('he') + italic('llo') + parse_emphasis("_*he_llo*"), (italic + underline)("he") + italic("llo") ) def test_complicated(self): # As reported by Stu self.assertEqual( parse_emphasis( - 'You can _underline_ words, make them **bold** or *italic* ' - 'or even ***bold italic.***' - ), ( - plain('You can ') + underline('underline') + - plain(' words, make them ') + bold('bold') + plain(' or ') + - italic('italic') + plain(' or even ') + - (bold + italic)('bold italic.') - ) + "You can _underline_ words, make them **bold** or *italic* " + "or even ***bold italic.***" + ), + ( + plain("You can ") + + underline("underline") + + plain(" words, make them ") + + bold("bold") + + plain(" or ") + + italic("italic") + + plain(" or even ") + + (bold + italic)("bold italic.") + ), ) def test_simplified_complicated(self): self.assertEqual( - parse_emphasis('*italic* or even ***bold italic.***'), - italic('italic') + plain(' or even ') + - (bold + italic)('bold italic.') + parse_emphasis("*italic* or even ***bold italic.***"), + italic("italic") + plain(" or even ") + (bold + italic)("bold italic."), ) def test_two_italic_should_not_create_one_long_italic_string(self): self.assertEqual( - parse_emphasis('*first* *second*'), - italic('first') + plain(' ') + italic('second') + parse_emphasis("*first* *second*"), + italic("first") + plain(" ") + italic("second"), ) def test_two_bold_should_not_create_one_long_bold_string(self): self.assertEqual( - parse_emphasis('**first** **second**'), - bold('first') + plain(' ') + bold('second') + parse_emphasis("**first** **second**"), + bold("first") + plain(" ") + bold("second"), ) def test_escaping_star_creates_a_literal_star(self): - self.assertEqual( - parse_emphasis(r'\*hello*'), - plain('*hello*') - ) + self.assertEqual(parse_emphasis(r"\*hello*"), plain("*hello*")) diff --git a/tests/visual/pdf_test.py b/tests/visual/pdf_test.py index 0059645..b25efc1 100755 --- a/tests/visual/pdf_test.py +++ b/tests/visual/pdf_test.py @@ -12,8 +12,9 @@ """ import subprocess -from pathlib import Path import sys +from pathlib import Path + import screenplain.main @@ -32,9 +33,9 @@ def compare(directory) -> bool: actual_file = fountain_file.with_stem( fountain_file.stem + "-actual" ).with_suffix(".pdf") - diff_path = actual_file.with_stem( - fountain_file.stem + "-diff" - ).with_suffix(".pdf") + diff_path = actual_file.with_stem(fountain_file.stem + "-diff").with_suffix( + ".pdf" + ) # Generate PDF using screenplain Python API screenplain.main.main([str(fountain_file), str(actual_file)]) diff --git a/uv.lock b/uv.lock index 09d85c1..5adda19 100644 --- a/uv.lock +++ b/uv.lock @@ -495,6 +495,9 @@ dev = [ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "ruff" }, + { name = "setuptools", version = "82.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "setuptools", version = "83.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ty" }, ] [package.metadata] @@ -504,6 +507,32 @@ requires-dist = [{ name = "reportlab", specifier = ">=4.4.7" }] dev = [ { name = "pytest", specifier = ">=8.4.2" }, { name = "ruff", specifier = ">=0.14.11" }, + { name = "setuptools", specifier = ">=82.0.1" }, + { name = "ty", specifier = ">=0.0.53" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "setuptools" +version = "83.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, ] [[package]] @@ -555,6 +584,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] +[[package]] +name = "ty" +version = "0.0.60" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/6e/1ab6f2727622d38ddfb2a7f994209b3087190b76885e2f754dbb6e58e0c9/ty-0.0.60.tar.gz", hash = "sha256:ebd7517d1aa8d8c3793cbf03c263679a42b939eca650df583234f92a5eb5837a", size = 6189323, upload-time = "2026-07-16T10:18:14.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/fc/c82d30b753dfb9d83ac34568478d9487bc42e1e79241a22b57f4b28fabee/ty-0.0.60-py3-none-linux_armv6l.whl", hash = "sha256:0842270c2e10d8416ca9f8aca1357c827efc5212300fbbc709e186e66fc7f9da", size = 11831443, upload-time = "2026-07-16T10:17:34.912Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/09e386d816076d1d90c57baee7f607e1475b625bb9f9279b28302da22f18/ty-0.0.60-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f49d024571cd7e569081638ab0ccc9e6795d642c6b9208addecc5e487364f0a3", size = 11624536, upload-time = "2026-07-16T10:17:37.445Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1e/e527e29fd9a3d41ddc419a43b37927c42a0d8ee4ca8a148ce4e022328f2f/ty-0.0.60-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b006c9a793d735ff808999775bbc0f07f91de50e931799f17322440cd9d055f", size = 11032920, upload-time = "2026-07-16T10:17:39.695Z" }, + { url = "https://files.pythonhosted.org/packages/79/e0/0f294bf2521f2836ba1a4682d4301c2038e4dfedee3a70df8f6005694978/ty-0.0.60-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:233033de40a0722420380d40f0eeebe8eb0d3afeeab3a6e7b1388e2caf1a017c", size = 11575334, upload-time = "2026-07-16T10:17:41.644Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8c/682e46e29165d9c94829576687092a8ea4556dd5882b11edd32bc7f4b534/ty-0.0.60-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61bcbd67f653ae2966d0e423879127907c6f8b567d64b6db93cb8f6cca7045ed", size = 11659354, upload-time = "2026-07-16T10:17:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d3/6baf7eb3cf9459416032285a10ffe1309f7e255e3f1974372bc41d44cfd3/ty-0.0.60-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b72e44a0be08ad778693d49b00b03bb784c1465d8265b3b392b355b34257a437", size = 12293710, upload-time = "2026-07-16T10:17:46.032Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/b287d78c93449fc5153891ad040be55e0718711ae7c0f2c54d3b88dd4d00/ty-0.0.60-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13b89f03a76f149350617345ae33bd332d2c6023dc5d560dd10a4f0210fba13a", size = 12837636, upload-time = "2026-07-16T10:17:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/e6/f2/3d8c77ca5f836fb6280231030df5a4504476a38d4554330d2be42125888e/ty-0.0.60-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30db8ebf6715da30afac62311ac773bf436ed503ed989cc9d4be4d3bdc3cbd43", size = 12383469, upload-time = "2026-07-16T10:17:50.567Z" }, + { url = "https://files.pythonhosted.org/packages/e8/98/958047501d74f9d0df357df28fea1de2afa18f1e6b2dc4a3ea4975c83e14/ty-0.0.60-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ecd6dc686f90c3bfe22c9b435e4a5325f5066f706bfe95c1616ef425b1f0654", size = 12132334, upload-time = "2026-07-16T10:17:52.718Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/b2853634da28aac01ea0ba8ef7695dc3d6c9aaf9c63dfdcf7c7435bc6c25/ty-0.0.60-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2292124558d839e31afd9829b11dda726ce65096b1feb80d14a1ddc7ccb0f28", size = 12359945, upload-time = "2026-07-16T10:17:55.143Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ae/bfecc71fcf1b833117bfb0efb76e14f5772874775274115ac94f64c5ba7a/ty-0.0.60-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:78812ec03da2c3141030ee1717fb09f0845eb552a77e79680c1cca598861ddaf", size = 11534812, upload-time = "2026-07-16T10:17:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/d6/23/1236c1ab9cd6b2daba9c558c9dcf04644f7b636f3bab3c15e74c7f80f5e1/ty-0.0.60-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a92c1dbc48f8b414ddcf199c1119054de00f8ee995528fa9d9c2e4ddd13cd0c1", size = 11677591, upload-time = "2026-07-16T10:17:59.747Z" }, + { url = "https://files.pythonhosted.org/packages/49/49/86c9230d9a8bf1755ca4b0165e7cb6bdfa2e495f0a105b24c6a27b5a542c/ty-0.0.60-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2c367d5ffe545004f0603e5599614062230678eeab980acd32850a7c19d60fc9", size = 11901279, upload-time = "2026-07-16T10:18:02.301Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0a/0c2e9904362d9c63700116feb192a3e10122eef49a417cde84068b2562fd/ty-0.0.60-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a94b1f99aa8f8ca5879341a5351af708b85ce19c7494db320c719a1e129d08e8", size = 12230659, upload-time = "2026-07-16T10:18:04.384Z" }, + { url = "https://files.pythonhosted.org/packages/84/4d/39c414f6b4bc944b2bac19c181438ee393c8eb34e9e21d97e5a148745f95/ty-0.0.60-py3-none-win32.whl", hash = "sha256:5ea2c85249581fb0060b9ff63b5313330f48930b2e50f6c85a5a322701626cc5", size = 11175286, upload-time = "2026-07-16T10:18:06.523Z" }, + { url = "https://files.pythonhosted.org/packages/a7/08/fdf4072d96c71b65fe98c9ca2ea5b3d2d0f6e20873bb2639e8c7827bd5f6/ty-0.0.60-py3-none-win_amd64.whl", hash = "sha256:15d83c3d793cb07840b8888c084cc2c49eb8acc29456a2fcdfbfd509c82526e5", size = 12249240, upload-time = "2026-07-16T10:18:08.701Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a2/83a496d717f712f894cf26690bcd9465ca23cd1c9139cac669d9cca45d14/ty-0.0.60-py3-none-win_arm64.whl", hash = "sha256:32e63228c3d21ddb192385aaf54501f19478be7b764f7572014d5110e53a9085", size = 11620140, upload-time = "2026-07-16T10:18:11.316Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"