ویکی‌نبشته fawikisource https://fa.wikisource.org/wiki/%D8%B5%D9%81%D8%AD%D9%87%D9%94_%D8%A7%D8%B5%D9%84%DB%8C MediaWiki 1.47.0-wmf.23 first-letter مدیا ویژه بحث کاربر بحث کاربر ویکی‌نبشته بحث ویکی‌نبشته پرونده بحث پرونده مدیاویکی بحث مدیاویکی الگو بحث الگو راهنما بحث راهنما رده بحث رده درگاه بحث درگاه پدیدآورنده بحث پدیدآورنده برگه گفتگوی برگه فهرست گفتگوی فهرست TimedText TimedText talk پودمان بحث پودمان Event Event talk الگو:شعر 10 1356 304000 302592 2026-10-11T20:24:47Z Hanooz 17889 304000 wikitext text/x-wiki <includeonly><span class="poem"><table class="{{#ifeq:{{{1|}}}|نستعلیق|nastaliq}}" style="margin: 0 auto; {{{سبک|}}}"><tr style="text-align:center;"><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr></includeonly><noinclude> {{توضیحات}} [[رده:الگوهای شعر]] </noinclude> 3r4oxlvxht606tvlp2nkk2tak6yxduc الگو:پایان شعر 10 1357 304007 302591 2026-10-11T20:24:59Z Hanooz 17889 304007 wikitext text/x-wiki </table></span><noinclude>{{توضیحات|الگو:شعر/توضیحات}}[[Category:الگوهای شعر|پایان شعر]]{{pp-template}}</noinclude> lfru2wp06wykhdkpvicqbuhm36jaoi7 الگو:بیت 10 2016 304006 302598 2026-10-11T20:24:57Z Hanooz 17889 304006 wikitext text/x-wiki <includeonly><tr style="text-align:center;"> <td style="text-align:left;">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;">&#160;</span></td> <td colspan="2" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{1}}}</span></td> <td style="width:2em;{{{سبک|}}}">{{کوچکتر۲|{{{شو|}}}}}</td> <td colspan="2" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{2}}}</span></td> <td style="text-align:right;"><span style="display:inline-block; width:2em;">&#160;</span>{{کوچکتر۲|{{{شچ|}}}}}</td></tr></includeonly><noinclude> {{توضیحات|الگو:شعر/توضیحات}} --- [[رده:الگوهای شعر|ب]] {{pp-template|small=yes}}</noinclude> l8abae220rfnxu5khajsbdfr7qenv2t الگو:شعر/توضیحات 10 21606 304009 303729 2026-10-11T20:27:11Z Hanooz 17889 304009 wikitext text/x-wiki {{زیرصفحه توضیحات}} این الگو برای نمایش اشعار به کار می‌رود. === کاربرد === قبل از بیت‌های شعر باید از الگوی <nowiki>{{شعر}}</nowiki> استفاده کنید.برای هر بیت از شعر به صورت زیر از الگوی '''ب''' (مخفف ''بیت'') استفاده کنید: <nowiki>{{ب|مصراع اول|مصراع دوم}}</nowiki> در پایان شعر نیز باید از الگوی <nowiki>{{پایان شعر}}</nowiki> استفاده کنید. این الگو برای نوشتن شعر در ویکی‌نبشته به کار می‌آید. === نمونه === دستورات زیر: <pre> {{شعر}} {{ب|دوش وقت سحر از غصه نجاتم دادند|واندر آن ظلمت شب آب حیاتم دادند}} {{م|بیخود از شعشهٔ پرتو ذاتم گردند}} {{م|باده از جام تجلی صفاتم دادند}} {{پایان شعر}} </pre> === نمونه نستعلیق === *نکته: در صورت استفاده از نستعلیق {{الگو|دارای نستعلیق}} را استفاده کنید تا اگر کاربر فونت نستعلیق را ندارد از آن را نصب و استفاده نماید. {{دارای نستعلیق}} دستورات زیر: <pre> {{شعر|نستعلیق}} {{ب|دوش وقت سحر از غصه نجاتم دادند|واندر آن ظلمت شب آب حیاتم دادند}} {{م|بیخود از شعشهٔ پرتو ذاتم گردند}} {{م|باده از جام تجلی صفاتم دادند}} {{پایان شعر}} </pre> چنین نتیجه می‌دهد: {{شعر|نستعلیق}} {{ب|دوش وقت سحر از غصه نجاتم دادند|واندر آن ظلمت شب آب حیاتم دادند}} {{م|بیخود از شعشهٔ پرتو ذاتم گردند}} {{م|باده از جام تجلی صفاتم دادند}} {{پایان شعر}} === نمایش ابیات به شکل نامتقارن === * به منظور نمایش ابیات به صورت ضربدری و نامتقارن از الگوی {{الگو|ب۱}} بجای {{الگو|ب}} استفاده کنید: <pre> {{شعر}} {{ب۱|آن همه شوکت و ناموس شهان آخر کار|چند سطری است که بر صفحهٔ دفتر گذرد}} {{ب۱|عاقبت در دو سه خط جمع شود از بد و نیک|آنچه یک عمر به دارا و سکندر گذرد}} {{پایان شعر}} </pre> نتیجه: {{شعر}} {{ب۱|آن همه شوکت و ناموس شهان آخر کار|چند سطری است که بر صفحهٔ دفتر گذرد}} {{ب۱|عاقبت در دو سه خط جمع شود از بد و نیک|آنچه یک عمر به دارا و سکندر گذرد}} {{پایان شعر}} <pre> {{شعر}} {{ب۱|ش=۲۵۵|شچ=۲۵۶|تورفتگی=5%|یوسف گم‌گشته بازآید بکنعان غم مخور|کلبهٔ احزان شود روزی گلستان غم مخور}} {{ب۱|تورفتگی=5%|ای دل غمدیده حالت به شود دل بد مکن|وین سر شوریده بازآید بسامان غم مخور}} {{پایان شعر}} </pre> نتیجه: {{شعر}} {{ب۱|ش=۲۵۵|شچ=۲۵۶|تورفتگی=5%|یوسف گم‌گشته بازآید بکنعان غم مخور|کلبهٔ احزان شود روزی گلستان غم مخور}} {{ب۱|تورفتگی=5%|ای دل غمدیده حالت به شود دل بد مکن|وین سر شوریده بازآید بسامان غم مخور}} {{پایان شعر}} <pre> {{شعر}} {{ب|ش=۲۰۱|شو=۲۰۲|شچ=۲۰۳|ابر و باد و مه و خورشید و فلک در کارند|تا تو نانی بکف آریّ و بغفلت نخوری}} {{ب۱|ش=۲۰۴|شچ=۲۰۵|همه از بهر تو سر گشته و فرمانبردار|شرط انصاف نباشد که تو فرمان نبری}} {{پایان شعر}} </pre> نتیجه: {{شعر}} {{ب|ش=۲۰۱|شو=۲۰۲|شچ=۲۰۳|ابر و باد و مه و خورشید و فلک در کارند|تا تو نانی بکف آریّ و بغفلت نخوری}} {{ب۱|ش=۲۰۴|شچ=۲۰۵|همه از بهر تو سر گشته و فرمانبردار|شرط انصاف نباشد که تو فرمان نبری}} {{پایان شعر}} === ایجاد فاصله بین ابیات و مصراع‌ها === هنگامی که بخواهید میان ابیات یا مصراع‌های یک قطعه شعر یک خط فاصله ایجاد کنید، می‌توانید از الگوهای ذیل استفاده کنید: * {{الگو|بیت خالی}} * {{الگو|مصراع خالی}} === نکات استفاده === * برای نوشتن به شکل نستعلیق با این الگو آن را به صورت <nowiki>{{شعر|نستعلیق}}</nowiki> به کار بگیرید. * اگر می‌خواهید در سمت راست یک بیت شمارهٔ آن را درج کنید، از پارامتر «ش» که مخفف «شماره» است استفاده کنید. همچنین برای قرار دادن شماره در وسط یک بیت از پارامتر «شو» و برای گنجاندن شماره در سمت چپ بیت از پارامتر «شچ» استفاده کنید. مثال: <pre> {{شعر}} {{ب|ش=۱۰۰|نیست آواز کسی آنجا که اوست|کی رسد جان کسی آنجا که اوست}} {{ب|شو=۱۰۱|صد هزاران طور از جان بر ترست|هر چه خواهم گفت او زان برتر است}} {{ب|شچ=۱۰۲|عقل در سودای او حیران بماند|جان ز عجز انگشت در دندان بماند}} {{م|ش=۱۰۳|ای مرغ سحر عشق ز پروانه بیاموز}} {{م|شچ=۱۰۴|کان سوخته را جان شد و آواز نیامد}} {{پایان شعر}} </pre> {{شعر}} {{ب|ش=۱۰۰|نیست آواز کسی آنجا که اوست|کی رسد جان کسی آنجا که اوست}} {{ب|شو=۱۰۱|صد هزاران طور از جان بر ترست|هر چه خواهم گفت او زان برتر است}} {{ب|شچ=۱۰۲|عقل در سودای او حیران بماند|جان ز عجز انگشت در دندان بماند}} {{م|ش=۱۰۳|ای مرغ سحر عشق ز پروانه بیاموز}} {{م|شچ=۱۰۴|کان سوخته را جان شد و آواز نیامد}} {{پایان شعر}} * اگر می‌خواهید که برای شعر سبک اضافه‌ای به کار بگیرید از <code>|سبک</code> استفاده کنید. *:برای رنگ پشت‌زمینه دلخواه <nowiki>{{شعر|سبک=background: blue}}</nowiki> *:رنگ دلخواه برای متن <nowiki>{{شعر|سبک=color: white}}</nowiki> *:برای قرار نگرفتن در وسط <nowiki>{{شعر|سبک=margin: 0}}</nowiki> نمونه‌ای از ترکیب هر سه:{{سخ}} <pre> {{شعر|سبک=background: blue; margin: 0; color: white}} {{ب|دوش وقت سحر از غصه نجاتم دادند|واندر آن ظلمت شب آب حیاتم دادند}} {{پایان شعر}} </pre> {{شعر|سبک=background: blue; margin: 0; color: white}} {{ب|دوش وقت سحر از غصه نجاتم دادند|واندر آن ظلمت شب آب حیاتم دادند}} {{پایان شعر}} === الگوهای مرتبط === * [[الگو:مصراع]] برای نوشتن تک مصراع‌ها * [[الگو:بیت]] برای نوشتن بیت‌ها * [[الگو:پایان شعر]] برای تمام کردن الگو 4kugucwr4rxc2la9pr3r75uk58oqup1 الگو:بدون منبع 10 22527 303997 303000 2026-10-11T18:31:02Z Hanooz 17889 ترجمه هوش مصنوعی 303997 wikitext text/x-wiki {{ambox |type=content |text='''منبع این متن مشخص نیست.'''<br /> {{smaller block|لطفاً برای کسب اطلاعات بیشتر و بررسی صحت منبع، [[{{TALKPAGENAME}}|صفحهٔ بحث این صفحه]] را ببینید. منظور از «منبع»، مکانی است که کاربران دیگر بتوانند نسخه‌ای از این اثر را در آن بیابند. ترجیحاً باید نسخه‌ای اسکن‌شده از اصل اثر باشد که بتوان آن را در [[commons:صفحهٔ اصلی|ویکی‌انبار]] بارگذاری و در [[راهنما:نمونه‌خوانی|نمونه‌خوانی]] کرد. در غیر این صورت، بهتر است نشانی اینترنتی (URL) ارائه شود. اگر هیچ‌یک از این موارد در دسترس نیست، لطفاً دلیل آن را در صفحهٔ بحث توضیح دهید.}} }} <includeonly>{{category handler | all = [[رده:آثار بدون منبع|{{PAGENAME}}]] | nocat = {{{nocat|}}} }}</includeonly><noinclude> {{documentation}} </noinclude> 0c1krax2pt35pyoh40yywond28f7zyp الگو:مصراع 10 31044 304003 302595 2026-10-11T20:24:52Z Hanooz 17889 304003 wikitext text/x-wiki <includeonly><tr style="text-align:center;"> <td colspan="2"style="text-align:left;">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;">&#160;</span></td> <td colspan="3" class="b" style="{{{سبک|}}}">{{{1}}}</td> <td colspan="2"style="text-align:right;"><span style="display:inline-block; width:2em;">&#160;</span>{{کوچکتر۲|{{{شچ|}}}}}</td> </tr> </includeonly><noinclude> {{توضیحات|الگو:شعر/توضیحات}} [[رده:الگوهای شعر|ب]] {{pp-template}}</noinclude> tw874jsmv4pyr4bgtzdz6cui9n635j8 کاربر:Hanooz/صفحه تمرین 2 38907 303982 303005 2026-10-11T17:00:18Z Hanooz 17889 جایگزینی صفحه با '1 {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}} 2 {{وزن بدایع سعدی|مفاعیلن|مفاعیلن|فعولن}} 3 {{#invoke:وزن بدایع سعدی|main|مفاعیلن|مفاعیلن|فعولن}}' 303982 wikitext text/x-wiki 1 {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}} 2 {{وزن بدایع سعدی|مفاعیلن|مفاعیلن|فعولن}} 3 {{#invoke:وزن بدایع سعدی|main|مفاعیلن|مفاعیلن|فعولن}} tnyoijg2xc081mak26xmhwjxsd5hlfl 303985 303982 2026-10-11T17:31:25Z Hanooz 17889 303985 wikitext text/x-wiki {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}} 5ufglv7g9s2mf74ojf9xhkmm09ebvuf الگو:بیت۱ 10 39723 304005 302597 2026-10-11T20:24:56Z Hanooz 17889 304005 wikitext text/x-wiki <includeonly> <tr style="text-align:center;"> <td style="text-align:left;">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;">&#160;</span></td> <td colspan="3" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{1}}}</span></td> <td colspan="2" width="{{{تورفتگی|20%}}}"></td> <td></td> </tr> <tr style="text-align:center;"> <td></td> <td colspan="2" width="{{{تورفتگی|20%}}}"></td> <td colspan="3" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{2}}}</span></td> <td style="text-align:right;"><span style="display:inline-block; width:2em;">&#160;</span>{{کوچکتر۲|{{{شچ|}}}}}</td> </tr> </includeonly><noinclude> {{توضیحات|الگو:شعر/توضیحات}} [[رده:الگوهای شعر|ب]] {{pp-template|small=yes}}</noinclude> 4vfs56h0cdhpfu4hwg83nfkzunvnjkc الگو:بیت نردبانی 10 41687 304004 302596 2026-10-11T20:24:54Z Hanooz 17889 304004 wikitext text/x-wiki <includeonly> <tr style="text-align:center;"> <td width="5%">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;">&#160;</span></td> <td width="50%" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{1}}}</span></td> <td width="45%"></td> </tr> <tr style="text-align:center;"> <td width="45%"></td> <td width="50%" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt">{{{2}}}</span></td> <td width="5%"><span style="display:inline-block; width:2em;">&#160;</span>{{کوچکتر۲|{{{شچ|}}}}}</td> </tr> </includeonly><noinclude> این الگو شبیه به الگوی بیت است با این تفاوت که ابیات را به شکل نردبانی نشان می‌دهد. البته فعلاً آزمایشی است. {{توضیحات|الگو:شعر/توضیحات}} --- [[رده:الگوهای شعر|ب]] {{pp-template|small=yes}}</noinclude> 0e5x3lksz0lzmj39fohg5nc3wvk35dy برگه:The Blind Owl.pdf/۵۶ 104 46696 303964 124156 2026-10-11T15:33:04Z Charan Gill 6444 /* هم‌سنجی‌شده */ 303964 proofread-page text/x-wiki <noinclude><pagequality level="4" user="Charan Gill" />{{سصم|۵۶||}}</noinclude>است روی کاغذ بیاورم– چون باین وسیله بهتر میتوانم افکار خودم را مرتب و منظم بکنم– آیا مقصودم نوشتن وصیت نامه است؟ هرگز. چون نه مال دارم که دیوان بخورد و نه دین دارم که شیطان ببرد، وانگهی چه چیزی روی زمین میتواند برایم کوچکترین ارزش را داشته باشد– آنچه که زندگی بوده است از دست داده‌ام، گذاشتم و خواستم از دستم برود و بعد از آنکه من رفتم، بدرک، میخواهد کسی کاغذ پاره‌های مرا بخواند، میخواهد هفتاد سال سیاه هم نخواند – من فقط برای این احتیاج نوشتن که عجالتاً برایم ضروری شده است مینویسم– من محتاجم، بیش از پیش محتاجم که افکار خودم را به موجود خیالی خودم، بسایه خودم ارتباط بدهم– این سایه شومی که جلو روشنائی پیه‌سوز روی دیوار خم شده و مثل اینست آنچه که مینویسم بدقت میخواند و می‌بلعد– این سایه حتماً بهتر از من میفهمد! فقط با سایه خودم خوب میتوانم حرف بزنم، اوست که مرا وادار بحرف زدن میکند، فقط او میتواند مرا بشناسد، او حتماً میفهمد ... میخواهم عصاره، نه، شراب تلخ زندگی خودم را چکه چکه در گلوی خشک سایه‌ام چکانیده باو بگویم: «این زندگی منست !» «هر کس دیروز مرا دیده، جوان شکسته و ناخوشی<noinclude></noinclude> kg9h7q6x0yc5k1ydnbgc0desujloa1x برگه:The Blind Owl.pdf/۵۷ 104 46697 303966 166959 2026-10-11T15:42:20Z Charan Gill 6444 /* هم‌سنجی‌شده */ 303966 proofread-page text/x-wiki <noinclude><pagequality level="4" user="Charan Gill" />{{سصم|۵۷||}}</noinclude>دیده است ولی امروز پیرمرد قوزی می‌بیند که موهای سفید، چشمهای واسوخته و لب شکری دارد. من میترسم از پنجره اطاقم به بیرون نگاه بکنم، در آینه بخودم نگاه بکنم، چون همه جا سایه‌های مضاعف خودم را می‌بینم– اما برای اینکه بتوانم زندگی خودم را برای سایه خمیده‌ام شرح بدهم باید یک حکایت نقل بکنم– اوه، چقدر حکایتهائی راجع به ایام طفولیت، راجع به عشق، جماع، عروسی و مرگ وجود دارد و هیچکدام حقیقت ندارند– من از قصه‌ها و عبارت پردازی خسته شده‌ام.– «من سعی خواهم کرد که این خوشه را بفشارم ولی آیا در آن کمترین اثر حقیقت وجود خواهد داشت یا نه– این را دیگر نمیدانم– من نمیدانم کجا هستم و این تکه آسمان بالای سرم، یا این چند وجب زمینی که رویش نشسته‌ام مال نیشابور یا بلخ و یا بنارس است– در هر صورت من بهیچ چیز اطمینان ندارم. «من از بس چیزهای متناقض دیده و حرفهای جور بجور شنیده‌ام و از بسکه دید چشمهایم روی سطح اشیاء مختلف ساییده شده– این قشر نازک و سختی که روح پشت آن پنهان است، حالا هیچ چیز را باور نمیکنم– به ثقل و ثبوت اشیاء، به حقایق آشکار<noinclude></noinclude> ezfyijkhenqm8regfdwdoeptfr9h6q5 الگو:بیت خالی 10 69657 304002 302594 2026-10-11T20:24:51Z Hanooz 17889 304002 wikitext text/x-wiki <includeonly><tr style="text-align:center;"> <td style="text-align:left;">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;">&#160;</span></td> <td colspan="2" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt"></span></td> <td style="width:2em;{{{سبک|}}}">{{کوچکتر۲|{{{شو|}}}}}</td> <td colspan="2" class="b" {{#if:{{{سبک|}}}|style="{{{سبک}}}"|}}><span class="beyt"></span></td> <td style="text-align:right;"><span style="display:inline-block; width:2em;">&#160;</span>{{کوچکتر۲|{{{شچ|}}}}}</td></tr></includeonly><noinclude> {{توضیحات|الگو:شعر/توضیحات}} --- [[رده:الگوهای شعر|بیت خالی]] {{pp-template|small=yes}}</noinclude> 8o2oqs3dxergtvn977g3dcxuh74anlh الگو:مصراع خالی 10 69658 304001 302593 2026-10-11T20:24:49Z Hanooz 17889 304001 wikitext text/x-wiki <includeonly><tr style="text-align:center;"> <td colspan="2"style="text-align:left;">{{کوچکتر۲|{{{ش|}}}}}<span style="display:inline-block; width:2em;">&#160;</span></td> <td colspan="3" class="b" style="{{{سبک|}}}"></td> <td colspan="2"style="text-align:right;"><span style="display:inline-block; width:2em;">&#160;</span>{{کوچکتر۲|{{{شچ|}}}}}</td> </tr> </includeonly><noinclude> {{توضیحات|الگو:شعر/توضیحات}} [[رده:الگوهای شعر|ب]] {{pp-template}}</noinclude> et864forbvml6qugtbdyxhixv71y7vq پودمان:Header/attribution data 828 79642 303979 299649 2026-10-11T16:47:56Z Hanooz 17889 303979 Scribunto text/plain -- Persian (fa.wikisource) localization of the English contributor-types -- config module. Field names (noun, prefix, param_name, special_cases, -- language_prefix, index) are unchanged, since another module reads this -- table by these keys - only the user-facing strings were translated. -- param_name values were kept in Latin script so existing-style template -- calls (e.g. |author=, |translator=) keep working. require('strict') local function anon(text) text = text or 'ناشناس' local cat = '' if mw.title.getCurrentTitle():inNamespaces(0, 114) then cat = '[[رده:' .. 'آثار نویسندگان ناشناس' .. ']]' end return '[[درگاه:متون ناشناس|' .. text .. ']]' .. cat end local function anon_nocat(text) return '[[درگاه:متون ناشناس|' .. (text or 'ناشناس') .. ']]' end local contrib_types = { {noun = 'نویسنده', prefix = 'اثر', param_name = 'author'}, {noun = 'منسوب به', prefix = 'منسوب به', param_name = 'attributed-to', special_cases = {}}, {noun = 'اقتباس‌کننده', prefix = 'اقتباس از', param_name = 'adapter'}, {noun = 'خلاصه‌کننده', prefix = 'خلاصه از', param_name = 'abridger'}, {noun = 'تصویرگر', prefix = 'تصویرگری از', param_name = 'illustrator'}, {noun = 'کارگردان', prefix = 'کارگردانی از', param_name = 'director'}, {noun = 'ترانه‌سرا', prefix = 'ترانه از', param_name = 'lyricist'}, {noun = 'لیبرتونویس', prefix = 'لیبرتو از', param_name = 'librettist'}, { noun = 'کتاب از', prefix = 'کتاب از', param_name = 'book-by', special_cases = { ['?'] = 'نویسنده نامعلوم', ['unknown'] = 'نویسنده نامعلوم', ['not mentioned'] = 'نویسنده نامعلوم', ['anon'] = anon, ['anonymous'] = anon, ['various'] = 'نویسنده‌های مختلف' } }, {noun = 'آهنگساز', prefix = 'آهنگسازی از', param_name = 'composer'}, {noun = 'تنظیم‌کننده', prefix = 'تنظیم از', param_name = 'arranger'}, { noun = 'مترجم', prefix = 'ترجمهٔ', param_name = 'translator', language_prefix = function(language_name) return 'ترجمه‌شده از ' .. language_name .. ' توسط' end, special_cases = { ['?'] = 'مترجم نامعلوم', ['unknown'] = 'مترجم نامعلوم', ['not mentioned'] = 'مترجم نامعلوم', ['anon'] = anon, ['anonymous'] = anon, ['various'] = 'مترجم‌های مختلف', ['wikisource'] = '[[:en:Wikisource:Translations|ویکی‌نبشتهٔ انگلیسی]]' } }, { noun = 'ویراستار', prefix = 'تصحیح', param_name = 'editor', special_cases = { ['?'] = 'ویراستار نامعلوم', ['unknown'] = 'ویراستار نامعلوم', ['not mentioned'] = 'ویراستار نامعلوم', ['anon'] = anon_nocat, ['anonymous'] = anon_nocat, ['various'] = 'ویراستار‌های مختلف' } }, {noun = 'گردآورنده', prefix = 'گردآوری از', param_name = 'compiler'} } for i = 1, #contrib_types do local noun = contrib_types[i]['noun'] contrib_types[i]['param_name'] = contrib_types[i]['param_name'] or string.gsub(noun, ' ', '-') contrib_types[i]['special_cases'] = contrib_types[i]['special_cases'] or { ['?'] = noun .. ' نامعلوم', ['unknown'] = noun .. ' نامعلوم', ['not mentioned'] = noun .. ' نامعلوم', ['anon'] = anon, ['anonymous'] = anon, ['various'] = noun .. '‌های مختلف' } contrib_types[i]['index'] = i end return contrib_types rug0tqmyijybaut2q89nm8jhch92sup الگو:ایضاً 10 83517 303967 303155 2026-10-11T15:44:40Z Hanooz 17889 303967 wikitext text/x-wiki <includeonly><templatestyles src="ایضاً/styles.css"/><!-- --><span class="__ditto"><span class="__ditto_hidden">{{{1}}}</span><span class="__ditto_text __ditto_text_align_{{#switch:{{{align|{{{3|}}}}}}|l|left=left|r|right=right|j|justify=justify|c|center|#default=center}} __ditto_text_overflow_{{#switch:{{{overflow|}}}|h|hidden=hidden|v|visible|#default=visible}}">{{#switch: {{{tooltip|}}} | on = {{tooltip|{{{2|„}}}|{{{1}}}}} | #default = {{{2|„}}} }}</span></span></includeonly><noinclude>{{documentation}}</noinclude> ph5hharpzzh70tol8gkcmfv312opfn0 الگو:ریاضی 10 86603 303971 303176 2026-10-11T15:49:23Z Hanooz 17889 303971 wikitext text/x-wiki <templatestyles src="ریاضی/styles.css"/><span class="wst-wikimath-texhtml {{#if:{{{big|}}}{{{size|}}}|wst-wikimath-texhtml-big}}" dir="ltr" {{#if:{{{size|}}}|style="font-size: {{{size}}};"}}>{{{1}}}</span><noinclude> {{Documentation}} </noinclude> l26ad2xo0wytr015prau2yovcbm02kl الگو:تک‌فاصله 10 89847 303970 303139 2026-10-11T15:48:41Z Hanooz 17889 303970 wikitext text/x-wiki <onlyinclude><templatestyles src="تک‌فاصله/styles.css" /><span class="wst-mono">{{{1}}}</span></onlyinclude><noinclude>{{documentation}}</noinclude> msnfazsbifkrqf5a6ophi6fzi1kt8r9 پودمان:Translations 828 94826 303969 297741 2026-10-11T15:48:24Z Hanooz 17889 303969 Scribunto text/plain require('strict') local p = {} local getArgs = require('Module:Arguments').getArgs local header = require('Module:Header')._header local lang = require('Module:Lang')._lang local TableTools = require('Module:TableTools') local function _translations(args) local current_frame = mw.getCurrentFrame() local current_title = mw.title.getCurrentTitle() args.translated = args.translated or current_title.text local headerArgs = TableTools.shallowClone(args) headerArgs.pretitle = 'ترجمه‌های فارسی ' headerArgs.section = '' -- header distinguishes nil and blank args in some cases headerArgs.noyearcat = true headerArgs.noyear = args.year == nil headerArgs.disambiguation = nil headerArgs['allow-explicit-formatting'] = args.translated ~= nil headerArgs.header_class = 'ws-dynlayout-disable' headerArgs['language-required'] = true headerArgs.original = nil headerArgs.translated = nil local original = (args.original and args.language and lang({language = args.language, text = args.original})) or args.original local translated = tostring(mw.html.create('i'):wikitext(args.translated)) if args.original and args.original ~= args.translated then headerArgs.title = original .. ' (' .. translated .. ')' else headerArgs.title = original or translated end local subNote = mw.html.create('div') :addClass('wst-sub-note') :tag('span') :attr('id', 'nofooter') :wikitext('ترجمه‌های فارسی ' .. tostring(mw.html.create('b'):wikitext(original or args.translated)) .. ' عبارتند از:') return table.concat({ current_frame:extensionTag('templatestyles', '', {src = 'Template:ترجمه‌ها/styles.css'}), header(headerArgs), current_frame:preprocess('__DISAMBIG__'), tostring(subNote), (current_title:inNamespaces(0, 114) and (args.category or '[[Category:' .. 'Translations pages' .. ']]')) or '' }) end function p.translations(frame) return _translations(getArgs(frame)) end return p e28lmqeydhxpnkf1zcp6wusgjnljz92 الگو:شعر۲/styles.css 10 97053 303999 303749 2026-10-11T20:16:25Z Hanooz 17889 303999 sanitized-css text/css /* * الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر» * این صفحه باید از نوع «Sanitized CSS» باشد؛ افزونه پیشوند .mw-parser-output * را خودش اضافه میکند. */ /* ─── جدول: پایهٔ چیدمان ────────────────────────────────────────────────── * border-collapse: collapse حیاتی است؛ بدون آن، جدولهای ۷ستونی با * table-layout: fixed عرض ستونها را درست جمع نمیکنند و ترازبندی * ناهمگون میشود. */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; text-align: center; } .poem-table td { padding: 0.15em 0; vertical-align: middle; } /* ─── فاصلهها ─────────────────────────────────────────────────────────── * فاصلهٔ دو مصراع در هر بیت = ستون میانی (۴.۷em) + حاشیهٔ درونی دو مصراع (۲×۰.۹em) * فاصلهٔ شماره تا مصراع = حاشیهٔ شماره (۱.۴em) + حاشیهٔ درونی مصراع (۰.۹em) */ .poem-m { padding-inline: 0.9em; } .poem-num-c, .poem-g { width: 4.7em; } .poem-s { width: 3.4em; } /* ─── نستعلیق ──────────────────────────────────────────────────────────── * خودبسنده است و به Common.css وابسته نیست. فهرست قلم باید با * cfg.nastaliqFonts در پودمان یکی باشد. */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ─── سلول مصراع: قاعدهٔ اصلی ترازبندی ─────────────────────────────────── * .beyt یک بلوک تمامعرض است تا text-align-last: justify قابلاعتماد کار کند. * (در بعضی مرورگرها ترازبندی روی <td> وقتی تنها فرزندش یک <span> inline * است، اعمال نمیشود.) ترتیب اولویت استایلها: * ۱) style اینلاین روی <td> (از ماژول: تراز + سبک کاربر) * ۲) .poem-m در همین فایل (پایه) * ۳) .poem-m .beyt روی بلوک داخلی */ .poem-m .beyt { display: block; width: 100%; text-align: inherit; text-align-last: inherit; word-spacing: normal; word-break: keep-all; overflow-wrap: break-word; hyphens: none; } /* پایه: پهن. اگر ماژول تراز دیگری بدهد، آن اینلاین برنده است. */ .poem-m { text-align: justify; text-align-last: justify; text-align-last: justify; text-align-last: justify; } /* مصراع تنها با تراز راست/چپ (شعر نو): ترازبندی نمیشود */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ─── شمارهٔ ابیات ─────────────────────────────────────────────────────── */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 1.4em; } .poem-num-l { text-align: right; padding-right: 1.4em; } /* ─── سلولهای فاصلهگذار و ردیف فاصله ─────────────────────────────────── */ .poem-indent, .poem-gapcell { padding: 0; } /* ─── چیدمان ثابت ──────────────────────────────────────────────────────── * ستونها را ردیف نامرئی .poem-sizer تعیین میکند. عرض و حداکثر عرض روی * جعبهٔ .poem-fixed مینشیند، پس همهٔ جدولهای یک شعر هماندازهاند. */ .poem-fixed .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; } .poem-fixed .b { overflow-wrap: break-word; } /* ─── شعر چندصفحهای ────────────────────────────────────────────────────── */ .poem-skip { display: none; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ─── صفحهٔ باریک (تلفن همراه) ─────────────────────────────────────────── */ @media (max-width: 30em) { .poem-m { padding-inline: 0.4em; } .poem-num-r { padding-left: 0.5em; } .poem-num-l { padding-right: 0.5em; } .poem-num-c, .poem-g { width: 2.4em; } .poem-s { width: 2em; } } l4npi65yls0eqobcxdzwrnmwzhm7yiq 304008 303999 2026-10-11T20:26:34Z Hanooz 17889 Hanooz صفحهٔ [[الگو:شعر/styles.css]] را بدون برجای‌گذاشتن تغییرمسیر به [[الگو:شعر۲/styles.css]] منتقل کرد 303999 sanitized-css text/css /* * الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر» * این صفحه باید از نوع «Sanitized CSS» باشد؛ افزونه پیشوند .mw-parser-output * را خودش اضافه میکند. */ /* ─── جدول: پایهٔ چیدمان ────────────────────────────────────────────────── * border-collapse: collapse حیاتی است؛ بدون آن، جدولهای ۷ستونی با * table-layout: fixed عرض ستونها را درست جمع نمیکنند و ترازبندی * ناهمگون میشود. */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; text-align: center; } .poem-table td { padding: 0.15em 0; vertical-align: middle; } /* ─── فاصلهها ─────────────────────────────────────────────────────────── * فاصلهٔ دو مصراع در هر بیت = ستون میانی (۴.۷em) + حاشیهٔ درونی دو مصراع (۲×۰.۹em) * فاصلهٔ شماره تا مصراع = حاشیهٔ شماره (۱.۴em) + حاشیهٔ درونی مصراع (۰.۹em) */ .poem-m { padding-inline: 0.9em; } .poem-num-c, .poem-g { width: 4.7em; } .poem-s { width: 3.4em; } /* ─── نستعلیق ──────────────────────────────────────────────────────────── * خودبسنده است و به Common.css وابسته نیست. فهرست قلم باید با * cfg.nastaliqFonts در پودمان یکی باشد. */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ─── سلول مصراع: قاعدهٔ اصلی ترازبندی ─────────────────────────────────── * .beyt یک بلوک تمامعرض است تا text-align-last: justify قابلاعتماد کار کند. * (در بعضی مرورگرها ترازبندی روی <td> وقتی تنها فرزندش یک <span> inline * است، اعمال نمیشود.) ترتیب اولویت استایلها: * ۱) style اینلاین روی <td> (از ماژول: تراز + سبک کاربر) * ۲) .poem-m در همین فایل (پایه) * ۳) .poem-m .beyt روی بلوک داخلی */ .poem-m .beyt { display: block; width: 100%; text-align: inherit; text-align-last: inherit; word-spacing: normal; word-break: keep-all; overflow-wrap: break-word; hyphens: none; } /* پایه: پهن. اگر ماژول تراز دیگری بدهد، آن اینلاین برنده است. */ .poem-m { text-align: justify; text-align-last: justify; text-align-last: justify; text-align-last: justify; } /* مصراع تنها با تراز راست/چپ (شعر نو): ترازبندی نمیشود */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ─── شمارهٔ ابیات ─────────────────────────────────────────────────────── */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 1.4em; } .poem-num-l { text-align: right; padding-right: 1.4em; } /* ─── سلولهای فاصلهگذار و ردیف فاصله ─────────────────────────────────── */ .poem-indent, .poem-gapcell { padding: 0; } /* ─── چیدمان ثابت ──────────────────────────────────────────────────────── * ستونها را ردیف نامرئی .poem-sizer تعیین میکند. عرض و حداکثر عرض روی * جعبهٔ .poem-fixed مینشیند، پس همهٔ جدولهای یک شعر هماندازهاند. */ .poem-fixed .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; } .poem-fixed .b { overflow-wrap: break-word; } /* ─── شعر چندصفحهای ────────────────────────────────────────────────────── */ .poem-skip { display: none; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ─── صفحهٔ باریک (تلفن همراه) ─────────────────────────────────────────── */ @media (max-width: 30em) { .poem-m { padding-inline: 0.4em; } .poem-num-r { padding-left: 0.5em; } .poem-num-l { padding-right: 0.5em; } .poem-num-c, .poem-g { width: 2.4em; } .poem-s { width: 2em; } } l4npi65yls0eqobcxdzwrnmwzhm7yiq 304020 304008 2026-10-11T21:19:18Z Hanooz 17889 304020 sanitized-css text/css /* * الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر» * این صفحه باید از نوع «Sanitized CSS» باشد؛ افزونه پیشوند .mw-parser-output * را خودش اضافه میکند. * * چیدمان واکنشگرا: * • > 45em : دو ستون، فاصلهٔ ۲em بین دو مصراع (مانند الگوهای اصلی) * • 30-45em: دو ستون، فاصلهٔ کمتر * • < 30em : هر مصراع یک خط، با فاصله بین بیتها */ /* ═══ پایهٔ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; /* ستونها مستقل از محتوا، ثابت */ text-align: center; /* معادل tr style="text-align:center" در الگوهای اصلی */ } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ فاصلهٔ بین دو مصراع (مطابق الگوهای اصلی: ۲em) ═══ * .poem-g روی ردیفِ سایزر (سلول ۴) و .poem-num-c روی خودِ سلولِ میانی * اثر میگذارند. هر دو باید یک اندازه باشند تا چیدمان ثابت درست کار کند. */ .poem-g, .poem-num-c { width: 2em; } /* سلول شمارهٔ کناری: جا برای شماره + فاصلهٔ ۲em تا مصراع */ .poem-s { width: 3.5em; } /* حاشیهٔ درونی مصراع: با فاصلهٔ ۲em میانی، فاصلهٔ دیداری ≈ ۳em */ .poem-m { padding-inline: 0.5em; } /* ═══ نستعلیق (خودبسنده، بدون وابستگی به Common.css) ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ سلول مصراع ═══ */ .poem-m .beyt { overflow-wrap: break-word; hyphens: none; } /* ═══ شمارهٔ ابیات ═══ * ش در RTL لبهٔ چپِ سلول = لبهٔ راستِ مصراع ⇒ با padding-left فاصله میگیرد * شچ در RTL لبهٔ راستِ سلول = لبهٔ چپِ مصراع ⇒ با padding-right فاصله میگیرد */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 2em; } .poem-num-l { text-align: right; padding-right: 2em; } /* ═══ مصراع تنها با تراز راست/چپ (شعر نو) ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ سلولهای فاصلهگذار و ردیف فاصله ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ چیدمان ثابت (ردیف نامرئیِ سایزر) ═══ */ .poem-fixed .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; } .poem-fixed .b { overflow-wrap: break-word; } /* ═══ شعر چندصفحهای ═══ */ .poem-skip { display: none; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══════════════════════════════════════════════════════════════════════ * صفحهٔ متوسط (۳۰em تا ۴۵em): * فاصلهٔ میانی کمتر، فاصلهٔ کنارهها کمتر، اما مصراعها همچنان کنار هم. * ═══════════════════════════════════════════════════════════════════════ */ @media (max-width: 45em) { .poem-g, .poem-num-c { width: 1.2em; } .poem-s { width: 2.2em; } .poem-m { padding-inline: 0.35em; } .poem-num-r { padding-left: 0.6em; } .poem-num-l { padding-right: 0.6em; } } /* ═══════════════════════════════════════════════════════════════════════ * صفحهٔ باریک (زیر ۳۰em): * هر مصراع یک خط کامل؛ پس از هر بیت (دو مصراع) فاصله و خطچینِ کمرنگ * تا پایانِ بیت دیده شود. شمارهها کوچک و بیآزار. * ═══════════════════════════════════════════════════════════════════════ */ @media (max-width: 30em) { /* جعبهٔ بیرونی عرض کامل صفحه را بگیرد */ .poem-fixed { width: 100% !important; max-width: none !important; } /* جدول از حالت جدول خارج شود تا سلولها روی هم بیفتند */ .poem-table { display: block; width: 100% !important; table-layout: auto; border-collapse: separate; border-spacing: 0; } .poem-sizer { display: none !important; } /* هر بیت یک بلوک مستقل با فاصله و جداکنندهٔ پایین */ .poem-row { display: block; padding: 0.3em 0 0.6em; margin-bottom: 0.7em; border-bottom: 1px dotted #c8c8c8; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } /* همهٔ سلولها بلوک تمامعرض */ .poem-row td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } /* شمارهٔ میانی (شو) در حالت عمودی معنا ندارد */ .poem-num-c { display: none !important; } /* شمارههای کناری: کوچک، بالای بیت، چسبیده به لبهها */ .poem-num-r, .poem-num-l { display: block; width: auto; font-size: 72%; line-height: 1.15; color: #888; padding: 0 0.4em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } /* هر مصراع روی خط خودش */ .poem-m { padding: 0.2em 0.4em !important; text-align: center !important; } /* بیتهای پلکانی/نردبانی: تورفتگی حذف، هر دو مصراع وسطچین */ .poem-indent, .poem-st { display: none !important; } .poem-stagger .poem-m { padding: 0.2em 0.4em !important; } } 6s7j2457rh0iulvhhm21h9rhdag6pdh 304021 304020 2026-10-11T21:33:37Z Hanooz 17889 304021 sanitized-css text/css /* * الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر» * این صفحه باید از نوع «Sanitized CSS» باشد. * * چیدمان واکنشگرا: * > 40em : دو ستون، فاصلهٔ میانی نسبیِ ۵٪ (روی جدول ۴۰em ≈ ۲em) * 32-40em : دو ستون، همان نسبت، فاصلهٔ کمکی کمتر * < 32em : هر مصراع یک خط، با خطچین بین بیتها */ /* ═══ پایهٔ جدول ═══ */ .poem-table { border-collapse: separate; border-spacing: 0; table-layout: fixed; /* عرض ستونها مستقل از محتوا */ text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ عرض ستونها (روی ردیف سایزر) ═══ * جمع = 6 + 20.75 + 20.75 + 5 + 20.75 + 20.75 + 6 = 100٪ * دو مصراع هر یک ۴۱.۵٪ جدول: هماندازه در همهٔ ابیات. */ .poem-s { width: 6%; } /* ستون ش و شچ */ .poem-mh { width: 20.75%; } /* هر نیم از یک مصراع */ .poem-g { width: 5%; } /* فاصلهٔ میانی */ /* حاشیهٔ داخلی مصراع (فاصلهٔ دیداری تا شماره و تا میانی) */ .poem-m { padding-inline: 0.4em; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ محتوای مصراع ═══ */ .poem-m .beyt { overflow-wrap: break-word; hyphens: none; } /* ═══ شمارهٔ ابیات ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.4em; } .poem-num-l { text-align: right; padding-right: 0.4em; } /* ═══ مصراع راست/چپ (شعر نو) ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ سایزر (ردیف نامرئی) ═══ */ .poem-fixed .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; } .poem-fixed .b { overflow-wrap: break-word; } /* ═══ شعر چندصفحهای ═══ */ .poem-skip { display: none; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط (۳۲em تا ۴۰em): فاصلهٔ میانی کمی کمتر ═══ */ @media (max-width: 40em) { .poem-s { width: 5%; } .poem-g { width: 4%; } .poem-mh { width: 21.5%; } /* 5 + 21.5×2 + 4 + 21.5×2 + 5 = 100 */ .poem-m { padding-inline: 0.3em; } } /* ═══ صفحهٔ باریک (زیر ۳۲em): هر مصراع یک خط ═══ * جدول به بلوک تبدیل میشود؛ میانی حذف؛ شمارهها کوچک بالای بیت. * بین هر دو مصراع (پایان بیت) فاصله و خطچین کمرنگ. */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-num-c { display: none !important; } .poem-num-r, .poem-num-l { display: block; width: auto; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } /* در حالت عمودی تورفتگی بیت۱ معنا ندارد */ .poem-stagger .poem-m { padding-inline: 0.3em !important; } } 6onuxykz705vey4z8lmnn22tejtcm2w 304025 304021 2026-10-11T21:50:02Z Hanooz 17889 304025 sanitized-css text/css /* * الگو:شعر/styles.css — شیوهنامهٔ TemplateStyles برای «پودمان:شعر» * این صفحه باید از نوع «Sanitized CSS» باشد. */ /* ═══ پایهٔ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: auto; /* برای آنکه ردیف سایزرِ پایین هم در عرض ستونها اثر بگذارد */ text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ ردیف سایزر: نامرئی ولی اثرگذار در عرض ستونها ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; overflow: hidden; } .poem-sz { display: block; height: 0; overflow: hidden; white-space: nowrap; line-height: 0; visibility: hidden; } /* ═══ عرض ثابت ستونهای کناری و میانی ═══ */ .poem-num-c, .poem-g { width: 2em; min-width: 2em; } .poem-s { width: 3.5em; min-width: 3.5em; } .poem-m { padding-inline: 0.5em; } .poem-m .beyt { white-space: nowrap; overflow-wrap: normal; hyphens: none; } .poem-m-empty { padding: 0; } /* ═══ شمارهها ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ (شعر نو) ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ شعر چندصفحهای ═══ */ .poem-skip { display: none; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط: فاصله کمتر ولی همچنان دوستونی ═══ */ @media (max-width: 45em) { .poem-num-c, .poem-g { width: 1.2em; min-width: 1.2em; } .poem-s { width: 2.2em; min-width: 2.2em; } .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.4em; } .poem-num-l { padding-right: 0.4em; } } /* ═══ صفحهٔ باریک: هر مصراع یک خط، با خطچین بین بیتها ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-num-c { display: none !important; } .poem-num-r, .poem-num-l { display: block; width: auto; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { white-space: normal; } .poem-m-empty { display: none !important; } } jvt9svjxiz2eu63oywi30gap9p1dg2r 304026 304025 2026-10-11T22:06:47Z Hanooz 17889 304026 sanitized-css text/css /* * الگو:شعر/styles.css — TemplateStyles برای «پودمان:شعر» */ /* ═══ پایهٔ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; /* عرض ستونها را سایزر تعیین میکند */ text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ عرض ستونها (روی ردیف سایزر) ═══ * ۵ + ۲۱ + ۲۱ + ۶ + ۲۱ + ۲۱ + ۵ = ۱۰۰ * هر مصراع (colspan=2) = ۴۲٪ جدول؛ فاصلهٔ میانی = ۶٪ */ .poem-s { width: 5%; } .poem-mh { width: 21%; } .poem-g { width: 6%; } /* ═══ سلولهای پُرنشده (فقط ساختاریاند) ═══ */ .poem-empty { padding: 0 !important; } /* ═══ حاشیهٔ درونی مصراع ═══ */ .poem-m { padding-inline: 0.4em; } .poem-m .beyt { overflow-wrap: break-word; hyphens: none; } /* ═══ شمارهٔ ابیات ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ (شعر نو) ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ سایزر (ردیف نامرئیِ تعیین عرض) ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; font-size: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ شعر چندصفحهای ═══ */ .poem-skip { display: none; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط: فاصلهٔ میانی کمتر ═══ */ @media (max-width: 45em) { .poem-s { width: 4%; } .poem-mh { width: 22%; } /* 4 + 22×4 + 4 + 4 = 100 */ .poem-g { width: 4%; } .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ صفحهٔ باریک: هر مصراع یک خط، خطچین پس از هر بیت ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } /* بیت۱ دو ردیف است: ردیف اول بدون جداکننده، ردیف دوم جداکننده دارد */ .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } /* سلولها روی هم میافتند */ .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } /* سلولهای خالی (پُرنشده) در حالت عمودی پنهان */ .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { white-space: normal; } } pks8299tghobqvbjpc8js96ns95jc6x 304031 304026 2026-10-11T22:22:11Z Hanooz 17889 304031 sanitized-css text/css /* * الگو:شعر/styles.css — TemplateStyles برای «پودمان:شعر» */ /* ═══ پایهٔ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; /* عرض ستونها را سایزر تعیین میکند */ text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ عرض ستونها (روی ردیف سایزر) ═══ * ۵ + ۲۱ + ۲۱ + ۶ + ۲۱ + ۲۱ + ۵ = ۱۰۰ * هر مصراع (colspan=2) = ۴۲٪ جدول؛ فاصلهٔ میانی = ۶٪ */ .poem-s { width: 5%; } .poem-mh { width: 21%; } .poem-g { width: 6%; } /* ═══ سلولهای پُرنشده (فقط ساختاریاند) ═══ */ .poem-empty { padding: 0 !important; width: auto !important; min-width: 0; border: 0; line-height: 0; font-size: 0; } /* ═══ حاشیهٔ درونی مصراع ═══ */ .poem-m { padding-inline: 0.4em; } .poem-m .beyt { overflow-wrap: break-word; hyphens: none; } /* ═══ شمارهٔ ابیات ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ (شعر نو) ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ سایزر (ردیف نامرئیِ تعیین عرض) ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; font-size: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ شعر چندصفحهای ═══ */ .poem-skip { display: none; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط: فاصلهٔ میانی کمتر ═══ */ @media (max-width: 45em) { .poem-s { width: 4%; } .poem-mh { width: 22%; } .poem-g { width: 4%; } .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ صفحهٔ باریک: هر مصراع یک خط، خطچین پس از هر بیت ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } /* بیت۱ دو ردیف است: ردیف اول بدون جداکننده، ردیف دوم جداکننده دارد */ .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } /* سلولها روی هم میافتند */ .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } /* سلولهای خالی (پُرنشده) در حالت عمودی پنهان */ .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { white-space: normal; } } 1b63sj2ruz4qrtsdp81o1ijl3ltvr2c 304033 304031 2026-10-11T22:33:49Z Hanooz 17889 304033 sanitized-css text/css /* * الگو:شعر/styles.css — TemplateStyles برای «پودمان:شعر» */ /* ═══ پایهٔ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ عرض ستونها ═══ */ .poem-s { width: 5%; } .poem-mh { width: 21%; } .poem-g { width: 6%; } /* ═══ سلولهای پُرنشده ═══ */ .poem-empty { padding: 0 !important; width: auto !important; min-width: 0; border: 0; line-height: 0; font-size: 0; } /* ═══ حاشیهٔ درونی مصراع ═══ */ .poem-m { padding-inline: 0.4em; } /* ═══ محتوای مصراع ═══ * ساختار: <span class="beyt"> * <span class="beyt-text">متن کشیدهشده</span> * <span class="beyt-refs"><sup>[۱]</sup>...</span> * </span> * .beyt-text در جریان ترازبندی شرکت میکند؛ .beyt-refs در انتهای * مصراع چسبیده به آخرین واژه میماند و از همکشیدگی جدا میشود. */ .poem-m .beyt { overflow-wrap: break-word; hyphens: none; } .poem-m .beyt-refs { white-space: nowrap; margin-inline-start: 0.15em; } .poem-m .beyt-refs sup { line-height: 1; } /* ═══ شمارهٔ ابیات ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ سایزر ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; font-size: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ شعر چندصفحهای ═══ */ .poem-skip { display: none; } .poem-row.poem-final-marker { display: none !important; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط ═══ */ @media (max-width: 45em) { .poem-s { width: 4%; } .poem-mh { width: 22%; } .poem-g { width: 4%; } .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ صفحهٔ باریک ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { white-space: normal; } .poem-m .beyt-refs { display: inline; } } rork2qvtyk6v1sm59rgn4r71vtkt8ut 304034 304033 2026-10-11T22:44:39Z Hanooz 17889 304034 sanitized-css text/css /* * الگو:شعر/styles.css — TemplateStyles برای «پودمان:شعر» */ /* ═══ پایهٔ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ عرض ستونها ═══ */ .poem-s { width: 5%; } .poem-mh { width: 21%; } .poem-g { width: 6%; } /* ═══ سلولهای پُرنشده ═══ */ .poem-empty { padding: 0 !important; width: auto !important; min-width: 0; border: 0; line-height: 0; font-size: 0; } /* ═══ حاشیهٔ درونی مصراع ═══ */ .poem-m { padding-inline: 0.4em; } /* ═══ محتوای مصراع (flex: متن + ارجاعها) ═══ * .beyt یک flex row است: .beyt-text با flex:1 کل فضای آزاد را میگیرد * (و درونش ترازبندی اعمال میشود)، .beyt-refs با flex:0 فقط اندازهٔ خودش * را میگیرد و در انتهای مصراع مینشیند. بهاینترتیب حضور ارجاع، متن را * کوتاه نمیکند. */ .poem-m .beyt { display: flex; align-items: baseline; width: 100%; gap: 0; } .poem-m .beyt-text { flex: 1 1 0; min-width: 0; overflow-wrap: break-word; hyphens: none; text-align: inherit; text-align-last: inherit; } .poem-m .beyt-refs { flex: 0 0 auto; white-space: nowrap; margin-inline-start: 0.15em; overflow-wrap: normal; } .poem-m .beyt-refs sup { line-height: 1; } /* ═══ شمارهٔ ابیات ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ سایزر ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; font-size: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ شعر چندصفحهای ═══ */ .poem-skip { display: none; } .poem-row.poem-final-marker { display: none !important; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط ═══ */ @media (max-width: 45em) { .poem-s { width: 4%; } .poem-mh { width: 22%; } .poem-g { width: 4%; } .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ صفحهٔ باریک ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { display: block; } .poem-m .beyt-text { display: inline; } .poem-m .beyt-refs { display: inline; margin-inline-start: 0.15em; } } jje7fociftyqv5zvoqhizfxc7oz5bda 304036 304034 2026-10-12T03:59:27Z Hanooz 17889 304036 sanitized-css text/css /* * الگو:شعر/styles.css — TemplateStyles برای «پودمان:شعر» */ /* ═══ جدول ═══ */ .poem-table { border-collapse: collapse; border-spacing: 0; table-layout: fixed; text-align: center; } .poem-table td { padding: 0.1em 0; vertical-align: middle; } /* ═══ عرض ستونها ═══ */ .poem-s { width: 5%; } .poem-mh { width: 21%; } .poem-g { width: 6%; } /* ═══ سلولهای خالی ساختاری ═══ */ .poem-empty { padding: 0 !important; width: auto !important; min-width: 0; border: 0; line-height: 0; font-size: 0; } /* ═══ مصراع ═══ * ترازبندی از style اینلاین (ALIGN_STYLE در پودمان) میآید و برنده است. * قاعدهٔ CSS زیر فقط پایه است، برای وقتی که سبک کاربر تراز ندهد. * ارجاعها بهصورت inline داخل متن میمانند و به آخرین واژه میچسبند، * پس حضورشان طول متنِ کشیدهشده را کوتاه نمیکند. */ .poem-m { padding-inline: 0.4em; } .poem-m .beyt { display: block; overflow-wrap: break-word; hyphens: none; text-align: inherit; text-align-last: inherit; } .poem-m .beyt sup.reference { white-space: nowrap; } /* ═══ شمارهٔ ابیات ═══ */ .poem-num { font-size: 85%; white-space: nowrap; vertical-align: middle; } .poem-num-r { text-align: left; padding-left: 0.5em; } .poem-num-l { text-align: right; padding-right: 0.5em; } /* ═══ مصراع راست/چپ ═══ */ .poem-m-right { text-align: right; text-align-last: auto; } .poem-m-left { text-align: left; text-align-last: auto; } /* ═══ فاصلهگذارها ═══ */ .poem-indent, .poem-gapcell { padding: 0; } /* ═══ سایزر ═══ */ .poem-sizer td { padding: 0; border: 0; height: 0; line-height: 0; font-size: 0; } /* ═══ نستعلیق ═══ */ .poem-table.nastaliq, .poem-table.nastaliq td, .poem-table.nastaliq .beyt { font-family: 'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', 'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif; } .poem-table.nastaliq { font-size: 125%; line-height: 2.1; } /* ═══ چندصفحهای ═══ */ .poem-skip { display: none; } .poem-row.poem-final-marker { display: none !important; } .poem-partial-end td.b { border-bottom: 1px dashed; } .poem-partial-start td.b { border-top: 1px dashed; } /* ═══ صفحهٔ متوسط ═══ */ @media (max-width: 45em) { .poem-s { width: 4%; } .poem-mh { width: 22%; } .poem-g { width: 4%; } .poem-m { padding-inline: 0.3em; } .poem-num-r { padding-left: 0.3em; } .poem-num-l { padding-right: 0.3em; } } /* ═══ صفحهٔ باریک ═══ */ @media (max-width: 32em) { .poem-fixed { width: 100% !important; max-width: none !important; } .poem-table { display: block; width: 100% !important; table-layout: auto; } .poem-table tbody { display: block; } .poem-sizer { display: none !important; } .poem-row { display: block; padding: 0.25em 0 0.5em; margin-bottom: 0.6em; border-bottom: 1px dotted #c8c8c8; } .poem-row.poem-stagger-1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; } .poem-row.poem-stagger-2 { border-bottom: 1px dotted #c8c8c8; margin-bottom: 0.6em; } .poem-row:last-child { border-bottom: none; margin-bottom: 0; } .poem-row > td { display: block; width: auto !important; padding: 0 !important; text-align: center !important; text-align-last: auto !important; } .poem-empty, .poem-num-c, .poem-g, .poem-gapcell { display: none !important; } .poem-num-r, .poem-num-l { display: block; font-size: 70%; line-height: 1.15; color: #888; padding: 0 0.3em !important; } .poem-num-r { text-align: right !important; } .poem-num-l { text-align: left !important; } .poem-m { padding: 0.15em 0.3em !important; text-align: center !important; text-align-last: auto !important; } .poem-m .beyt { white-space: normal; } } ppmcxnvudx8f4o9gt5jf8hkdyp8ugmw پودمان:شعر 828 97054 303998 303748 2026-10-11T20:15:47Z Hanooz 17889 303998 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی الگو نقطهٔ ورود در {{#invoke:شعر|…}} {{شعر}} open آغاز شعر؛ با پارامتر «متن» کل شعر را میسازد {{پایان شعر}} close {{بیت}} bayt بیت دومصراعی {{بیت۱}} stagger بیت پلکانی (ضربدری) {{بیت نردبانی}} ladder بیت نردبانی (تورفتگی پیشفرض بیشتر) {{مصراع}} mesra یک مصراع {{بیت خالی}} emptyBayt {{مصراع خالی}} emptyMesra ساختار خروجی همان جدول ۷ستونی پیشین است (ظاهر شعرهای موجود و خروجی کتاب الکترونیکی تغییری نمیکند). شیوهنامه: «الگو:شعر/styles.css». چیدمان (تازه): جدول پیشفرض عرض ثابت و مستقل از محتوا دارد (عرض=، حداکثر عرض=)، تا همهٔ ابیات شعر — حتی در جدولهای جدا (صفحههای جدا) — از یک نقطه آغاز شوند. ترازبندی=وسط|راست|چپ|پهن|لبه برای هر نوع بیت؛ قلم=، نستعلیق خودبسنده (بدون Common.css). در حالت «متن»، خطی که با @ آغاز شود نوع ردیفهای بعدی را عوض میکند: «@بیت نردبانی». ترازبندی پیشفرض «پهن» (دوطرفه) است؛ اگر دو مصراع از نظر طول بسیار ناهمسان باشند (نسبت کمتر از cfg.justifyMinRatio)، به تراز «لبه» فرو میافتد (مصراع اول راست، دوم چپ — مانند چاپ سنگی). این رفتار با پارامتر «ترازبندی» قابل تغییر است. شعر چندصفحهای (ایدهٔ start/end در ppoem): • در فضای نام «صفحه» هر صفحه همیشه خودبسنده و بسته نمایش داده میشود. • در صفحهٔ اصلی (درج با <pages/>) پارامتر «ادامه» صفحهها را به هم میدوزد: {{شعر|ادامه}} جدول تازه باز نمیکند و {{پایان شعر|ادامه}} نمیبندد. • ردیفِ ناتمام (مصراعی که در صفحهٔ بعد ادامه دارد) نگه داشته میشود (ناتمام=بله) و با نخستین ردیفِ صفحهٔ بعد (دنباله=بله) یکی میشود. این رفتار اکنون پیشفرض خاموش است (cfg.mergeRows = false) تا در صفحهٔ اصلی هر تکه مستقل نمایش داده شود؛ درج صفحهها معمولاً خودش کافی است. ]==] local p = {} --------------------------------------------------------------------------- -- تنظیمات --------------------------------------------------------------------------- local ZWNJ = '\226\128\140' -- نیمفاصله local cfg = { styles = 'Template:شعر/styles.css', pageNs = 104, defaultIndent = '20%', ladderIndent = '45%', mesraUnit = '1.5em', nbsp = '&#160;', -- چیدمان: 'auto' = جدول هماندازهٔ محتوا (رفتار قدیمی)؛ طول/درصد = عرض ثابت defaultWidth = '100%', maxWidth = '42em', -- باریکتر تا کشش واژهها در ترازبندی کم شود -- ترازبندی پیشفرض هر نوع -- بیت → پهن (دوطرفه) ، پلکانی → لبه justDefault = { bayt = 'justify', stagger = 'edge' }, -- وقتی نسبت طول کوتاهترین مصراع به بلندترین از این کمتر باشد، «پهن» به -- این تراز فرو میافتد. «لبه» شبیه چاپ سنگی است: مصراع اول به لبهٔ بیرونی راست، -- دومی به لبهٔ بیرونی چپ؛ کنارهٔ میانی ناهموار میماند. justFallback = 'edge', -- آستانهٔ فروافتادن از «پهن» به justFallback justifyMinRatio = 0.7, -- قلم نستعلیق (همین فهرست در CSS هم آمده) nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", -- ادغام ردیف ناتمام در صفحهٔ اصلی: پیشفرض خاموش (هر تکه مستقل نمایش داده میشود). -- اگر واقعاً میخواهید مصراعی که در مرز صفحه قطع شده یکجا نشان داده شود، -- آن را true کنید. برای پیشنمایش فضای نام «صفحه» تفاوتی ندارد. mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } -- نام پارامترها (فارسی + معادل انگلیسی) local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep', } local YES = { ['بله'] = true, ['بلی'] = true, ['آری'] = true, ['خودکار'] = true, yes = true, y = true, auto = true, ['true'] = true, ['1'] = true, } local NO = { ['نه'] = true, ['خیر'] = true, no = true, n = true, ['false'] = true, ['0'] = true, } local UNITS = { [''] = 'px', ['%'] = '%', px = 'px', em = 'em', ex = 'ex', ch = 'ch', rem = 'rem', vw = 'vw', vh = 'vh', pt = 'pt', cm = 'cm', mm = 'mm', ['in'] = 'in', } local CONT = { ['قبل'] = 'prev', ['پیشین'] = 'prev', prev = 'prev', ['بعد'] = 'next', ['پسین'] = 'next', next = 'next', ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', } local JOIN = { ['فاصله'] = ' ', space = ' ', ['چسبیده'] = '', ['بدون فاصله'] = '', tight = '', ['نیم' .. ZWNJ .. 'فاصله'] = ZWNJ, ['نیمفاصله'] = ZWNJ, ['نیم فاصله'] = ZWNJ, zwnj = ZWNJ, } local WHICH = { ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', ['اول'] = 'first', first = 'first', ['دوم'] = 'second', second = 'second', } local STANZA = { ['جدید'] = true, new = true } local JUST = { ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', ['راست'] = 'right', right = 'right', ['چپ'] = 'left', left = 'left', ['پهن'] = 'justify', ['دوطرفه'] = 'justify', ['دو' .. ZWNJ .. 'طرفه'] = 'justify', justify = 'justify', ['لبه'] = 'edge', edge = 'edge', } -- ترازبندی بهصورت inline روی سلول مصراع. عمداً «صریح» است تا CSS محلی ویکی -- نتواند تراز را بازنویسی کند؛ ترتیب: ترازِ ماژول، سپس سبک کاربر. local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;' .. '-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar = true, arz = true, azb = true, bgn = true, ckb = true, dv = true, fa = true, glk = true, he = true, ks = true, lrc = true, mzn = true, pnb = true, prs = true, ps = true, sd = true, ug = true, ur = true, yi = true, } -- وضعیتِ میان فراخوانیهای یک صفحه local state = { ctx = nil, open = false, carry = nil, } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' --------------------------------------------------------------------------- -- ابزارهای عمومی --------------------------------------------------------------------------- local function esc(s) s = s:gsub('"', '&quot;'):gsub('<', '&lt;'):gsub('>', '&gt;') return s end local function pick(args, names) for _, name in ipairs(names) do local v = args[name] if v and v ~= '' then return v end end return nil end local function present(args, names) for _, name in ipairs(names) do if args[name] ~= nil then return true end end return false end local function bool(v, default) if v == nil then return default end v = v:lower() if YES[v] then return true end if NO[v] then return false end return default end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end s = mw.text.trim(s) s = s:gsub('%s*\n%s*', ' ') return s end local function getArgs(frame) local args = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then args[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return args end local function hasUnknown(args, positional) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > positional or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn', 1, true) or rest:find('cyrl', 1, true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab', 1, true) or rest:find('hebr', 1, true) then return 'rtl' end return 'ltr' end -- تزریق TemplateStyles با محافظت pcall (اگر صفحهٔ CSS نبود یا افزونه خاموش بود، خطا ندهد) local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end --------------------------------------------------------------------------- -- ارقام --------------------------------------------------------------------------- local FA = { [0] = '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names) if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end --------------------------------------------------------------------------- -- اندازههای CSS --------------------------------------------------------------------------- local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin') v = v:gsub('٪', '%%'):gsub('٫', '.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local num, unit = splitLength(v) if not num then return nil end return string.format('%g', num) .. unit end --------------------------------------------------------------------------- -- شمارهگذاری خودکار --------------------------------------------------------------------------- local function newCtx(args, kind, flags) local ctx = { digits = 'fa', count = 0, start = 1, side = 'l', mesra = (kind == 'mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()] if not ctx.digits then ctx.digits = 'fa'; flags.unknown = true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9) if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()] if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end --------------------------------------------------------------------------- -- ساخت HTML --------------------------------------------------------------------------- local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end -- ترازبندی هر مصراع. which = 1 (اول) یا 2 (دوم). -- j = 'center' → وسط، 'edge' → مصراع اول راست (start)، دوم چپ (end)، -- 'justify' → دوطرفه، 'right'/'left' → راست/چپ. local function alignFor(j, which) if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j or 'center'] or ALIGN_STYLE.center end -- طول تقریبی متن دیدنی (نشانگر strip، تگ HTML، پیشوند پیوند، نیمفاصله حذف میشوند) local function charLen(s) s = s:gsub('\127[^\127]*\127', '') :gsub('<[^>]*>', '') :gsub('%[%[[^%]|]*|', '') :gsub('[%[%]]', '') :gsub('\226\128\140', '') return mw.ustring.len(s) end -- اگر دو مصراع از نظر طول بسیار ناهمسان باشند، «پهن» به justFallback فرو میافتد. local function effectiveJust(j, a, b) if j == 'justify' then local la, lb = charLen(a or ''), charLen(b or '') local hi = math.max(la, lb) if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end end return j end local function td(class, colspan, style, content) local s = '<td class="' .. class .. '"' if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemi(text) return '<span class="beyt">' .. text .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end -- {{بیت}} local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2) return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, nil, d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemi(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemi(d.m2)) .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end local function boxWidth(indent) local num, unit = indent:match('^([%d%.]+)(.*)$') if unit == '%' then return string.format('%g%%', math.max(100 - tonumber(num), 10)) end return 'calc(100% - ' .. indent .. ')' end -- {{بیت۱}} و {{بیت نردبانی}} local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2) local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) if d.fixed then local bw = boxWidth(d.indent) return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, nil, d.r) .. td('b poem-m poem-m1', 5, s1, '<div class="poem-st" style="width:' .. bw .. ';margin-inline-end:auto">' .. hemi(m1) .. '</div>') .. td('poem-num', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num', 1, nil, '') .. td('b poem-m poem-m2', 5, s2, '<div class="poem-st" style="width:' .. bw .. ';margin-inline-start:auto">' .. hemi(d.m2) .. '</div>') .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end local sp = 'width:' .. d.indent return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, nil, d.r) .. td('b poem-m poem-m1', 3, s1, hemi(m1)) .. td('poem-indent', 2, sp, '') .. td('poem-num', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num', 1, nil, '') .. td('poem-indent', 2, sp, '') .. td('b poem-m poem-m2', 3, s2, hemi(d.m2)) .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end -- {{مصراع}} local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end -- ترازبندی صریح (مصراع تکنفره هرگز ترازبندی نمیشود مگر خود کاربر بخواهد) local inline = (align == 'right') and ALIGN_STYLE.right or (align == 'left') and ALIGN_STYLE.left or (align == 'justify') and ALIGN_STYLE.justify or ALIGN_STYLE.center local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], nil, d.r) .. td(cellClass, cs[2], style, content) .. td('poem-num poem-num-l', cs[3], nil, d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- ردیف نامرئی نخست در چیدمان ثابت: پهنای ستونها را مستقل از محتوا تعیین میکند local function sizerRow() return '<tr class="poem-sizer">' .. '<td class="poem-s"></td><td></td><td></td>' .. '<td class="poem-g"></td><td></td><td></td>' .. '<td class="poem-s"></td></tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- شعر چندصفحهای: نگهداشتن ردیف ناتمام و یکیکردن آن با دنبالهاش --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end --------------------------------------------------------------------------- -- خواندن پارامترها --------------------------------------------------------------------------- local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil) if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil) if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()] if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style) o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()] if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()] if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g) if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind) if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength(cfg.mesraUnit) if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()] if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w) if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx) if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end --------------------------------------------------------------------------- -- حالت «متن»: ساخت کل شعر از خطهای ساده --------------------------------------------------------------------------- local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*') s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center' s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right' s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v) if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv] if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv] if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') local indent if c.kind == 'stagger' then indent = c.indent or cfg.defaultIndent elseif c.kind == 'ladder' then indent = c.indent or cfg.ladderIndent end return twoH, rowKind, indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1) numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false) pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true) pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end --------------------------------------------------------------------------- -- نقطههای ورود --------------------------------------------------------------------------- local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end if cont and not inPageNs() then return SKIP .. categories(flags) end local html = flushHtml() state.open, state.ctx = false, nil if html ~= '' then html = html .. '\n' end return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()] if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', '' html = rowBayt(d) else d.text, d.blank = '', true html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(cfg.defaultIndent)) p.ladder = safe(staggerFn(cfg.ladderIndent)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p 4h2z88q4a2bz5s8zbo9cnt92vbsrgvk 304011 303998 2026-10-11T20:29:57Z Hanooz 17889 304011 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی الگو نقطهٔ ورود در {{#invoke:شعر|…}} {{شعر}} open آغاز شعر؛ با پارامتر «متن» کل شعر را میسازد {{پایان شعر}} close {{بیت}} bayt بیت دومصراعی {{بیت۱}} stagger بیت پلکانی (ضربدری) {{بیت نردبانی}} ladder بیت نردبانی (تورفتگی پیشفرض بیشتر) {{مصراع}} mesra یک مصراع {{بیت خالی}} emptyBayt {{مصراع خالی}} emptyMesra ساختار خروجی همان جدول ۷ستونی پیشین است (ظاهر شعرهای موجود و خروجی کتاب الکترونیکی تغییری نمیکند). شیوهنامه: «الگو:شعر/styles.css». چیدمان (تازه): جدول پیشفرض عرض ثابت و مستقل از محتوا دارد (عرض=، حداکثر عرض=)، تا همهٔ ابیات شعر — حتی در جدولهای جدا (صفحههای جدا) — از یک نقطه آغاز شوند. ترازبندی=وسط|راست|چپ|پهن|لبه برای هر نوع بیت؛ قلم=، نستعلیق خودبسنده (بدون Common.css). در حالت «متن»، خطی که با @ آغاز شود نوع ردیفهای بعدی را عوض میکند: «@بیت نردبانی». ترازبندی پیشفرض «پهن» (دوطرفه) است؛ اگر دو مصراع از نظر طول بسیار ناهمسان باشند (نسبت کمتر از cfg.justifyMinRatio)، به تراز «لبه» فرو میافتد (مصراع اول راست، دوم چپ — مانند چاپ سنگی). این رفتار با پارامتر «ترازبندی» قابل تغییر است. شعر چندصفحهای (ایدهٔ start/end در ppoem): • در فضای نام «صفحه» هر صفحه همیشه خودبسنده و بسته نمایش داده میشود. • در صفحهٔ اصلی (درج با <pages/>) پارامتر «ادامه» صفحهها را به هم میدوزد: {{شعر|ادامه}} جدول تازه باز نمیکند و {{پایان شعر|ادامه}} نمیبندد. • ردیفِ ناتمام (مصراعی که در صفحهٔ بعد ادامه دارد) نگه داشته میشود (ناتمام=بله) و با نخستین ردیفِ صفحهٔ بعد (دنباله=بله) یکی میشود. این رفتار اکنون پیشفرض خاموش است (cfg.mergeRows = false) تا در صفحهٔ اصلی هر تکه مستقل نمایش داده شود؛ درج صفحهها معمولاً خودش کافی است. ]==] local p = {} --------------------------------------------------------------------------- -- تنظیمات --------------------------------------------------------------------------- local ZWNJ = '\226\128\140' -- نیمفاصله local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, defaultIndent = '20%', ladderIndent = '45%', mesraUnit = '1.5em', nbsp = '&#160;', -- چیدمان: 'auto' = جدول هماندازهٔ محتوا (رفتار قدیمی)؛ طول/درصد = عرض ثابت defaultWidth = '100%', maxWidth = '42em', -- باریکتر تا کشش واژهها در ترازبندی کم شود -- ترازبندی پیشفرض هر نوع -- بیت → پهن (دوطرفه) ، پلکانی → لبه justDefault = { bayt = 'justify', stagger = 'edge' }, -- وقتی نسبت طول کوتاهترین مصراع به بلندترین از این کمتر باشد، «پهن» به -- این تراز فرو میافتد. «لبه» شبیه چاپ سنگی است: مصراع اول به لبهٔ بیرونی راست، -- دومی به لبهٔ بیرونی چپ؛ کنارهٔ میانی ناهموار میماند. justFallback = 'edge', -- آستانهٔ فروافتادن از «پهن» به justFallback justifyMinRatio = 0.7, -- قلم نستعلیق (همین فهرست در CSS هم آمده) nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", -- ادغام ردیف ناتمام در صفحهٔ اصلی: پیشفرض خاموش (هر تکه مستقل نمایش داده میشود). -- اگر واقعاً میخواهید مصراعی که در مرز صفحه قطع شده یکجا نشان داده شود، -- آن را true کنید. برای پیشنمایش فضای نام «صفحه» تفاوتی ندارد. mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } -- نام پارامترها (فارسی + معادل انگلیسی) local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep', } local YES = { ['بله'] = true, ['بلی'] = true, ['آری'] = true, ['خودکار'] = true, yes = true, y = true, auto = true, ['true'] = true, ['1'] = true, } local NO = { ['نه'] = true, ['خیر'] = true, no = true, n = true, ['false'] = true, ['0'] = true, } local UNITS = { [''] = 'px', ['%'] = '%', px = 'px', em = 'em', ex = 'ex', ch = 'ch', rem = 'rem', vw = 'vw', vh = 'vh', pt = 'pt', cm = 'cm', mm = 'mm', ['in'] = 'in', } local CONT = { ['قبل'] = 'prev', ['پیشین'] = 'prev', prev = 'prev', ['بعد'] = 'next', ['پسین'] = 'next', next = 'next', ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', } local JOIN = { ['فاصله'] = ' ', space = ' ', ['چسبیده'] = '', ['بدون فاصله'] = '', tight = '', ['نیم' .. ZWNJ .. 'فاصله'] = ZWNJ, ['نیمفاصله'] = ZWNJ, ['نیم فاصله'] = ZWNJ, zwnj = ZWNJ, } local WHICH = { ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', ['اول'] = 'first', first = 'first', ['دوم'] = 'second', second = 'second', } local STANZA = { ['جدید'] = true, new = true } local JUST = { ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', ['راست'] = 'right', right = 'right', ['چپ'] = 'left', left = 'left', ['پهن'] = 'justify', ['دوطرفه'] = 'justify', ['دو' .. ZWNJ .. 'طرفه'] = 'justify', justify = 'justify', ['لبه'] = 'edge', edge = 'edge', } -- ترازبندی بهصورت inline روی سلول مصراع. عمداً «صریح» است تا CSS محلی ویکی -- نتواند تراز را بازنویسی کند؛ ترتیب: ترازِ ماژول، سپس سبک کاربر. local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;' .. '-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar = true, arz = true, azb = true, bgn = true, ckb = true, dv = true, fa = true, glk = true, he = true, ks = true, lrc = true, mzn = true, pnb = true, prs = true, ps = true, sd = true, ug = true, ur = true, yi = true, } -- وضعیتِ میان فراخوانیهای یک صفحه local state = { ctx = nil, open = false, carry = nil, } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' --------------------------------------------------------------------------- -- ابزارهای عمومی --------------------------------------------------------------------------- local function esc(s) s = s:gsub('"', '&quot;'):gsub('<', '&lt;'):gsub('>', '&gt;') return s end local function pick(args, names) for _, name in ipairs(names) do local v = args[name] if v and v ~= '' then return v end end return nil end local function present(args, names) for _, name in ipairs(names) do if args[name] ~= nil then return true end end return false end local function bool(v, default) if v == nil then return default end v = v:lower() if YES[v] then return true end if NO[v] then return false end return default end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end s = mw.text.trim(s) s = s:gsub('%s*\n%s*', ' ') return s end local function getArgs(frame) local args = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then args[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return args end local function hasUnknown(args, positional) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > positional or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn', 1, true) or rest:find('cyrl', 1, true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab', 1, true) or rest:find('hebr', 1, true) then return 'rtl' end return 'ltr' end -- تزریق TemplateStyles با محافظت pcall (اگر صفحهٔ CSS نبود یا افزونه خاموش بود، خطا ندهد) local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end --------------------------------------------------------------------------- -- ارقام --------------------------------------------------------------------------- local FA = { [0] = '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names) if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end --------------------------------------------------------------------------- -- اندازههای CSS --------------------------------------------------------------------------- local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin') v = v:gsub('٪', '%%'):gsub('٫', '.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local num, unit = splitLength(v) if not num then return nil end return string.format('%g', num) .. unit end --------------------------------------------------------------------------- -- شمارهگذاری خودکار --------------------------------------------------------------------------- local function newCtx(args, kind, flags) local ctx = { digits = 'fa', count = 0, start = 1, side = 'l', mesra = (kind == 'mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()] if not ctx.digits then ctx.digits = 'fa'; flags.unknown = true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9) if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()] if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end --------------------------------------------------------------------------- -- ساخت HTML --------------------------------------------------------------------------- local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end -- ترازبندی هر مصراع. which = 1 (اول) یا 2 (دوم). -- j = 'center' → وسط، 'edge' → مصراع اول راست (start)، دوم چپ (end)، -- 'justify' → دوطرفه، 'right'/'left' → راست/چپ. local function alignFor(j, which) if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j or 'center'] or ALIGN_STYLE.center end -- طول تقریبی متن دیدنی (نشانگر strip، تگ HTML، پیشوند پیوند، نیمفاصله حذف میشوند) local function charLen(s) s = s:gsub('\127[^\127]*\127', '') :gsub('<[^>]*>', '') :gsub('%[%[[^%]|]*|', '') :gsub('[%[%]]', '') :gsub('\226\128\140', '') return mw.ustring.len(s) end -- اگر دو مصراع از نظر طول بسیار ناهمسان باشند، «پهن» به justFallback فرو میافتد. local function effectiveJust(j, a, b) if j == 'justify' then local la, lb = charLen(a or ''), charLen(b or '') local hi = math.max(la, lb) if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end end return j end local function td(class, colspan, style, content) local s = '<td class="' .. class .. '"' if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemi(text) return '<span class="beyt">' .. text .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end -- {{بیت}} local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2) return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, nil, d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemi(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemi(d.m2)) .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end local function boxWidth(indent) local num, unit = indent:match('^([%d%.]+)(.*)$') if unit == '%' then return string.format('%g%%', math.max(100 - tonumber(num), 10)) end return 'calc(100% - ' .. indent .. ')' end -- {{بیت۱}} و {{بیت نردبانی}} local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2) local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) if d.fixed then local bw = boxWidth(d.indent) return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, nil, d.r) .. td('b poem-m poem-m1', 5, s1, '<div class="poem-st" style="width:' .. bw .. ';margin-inline-end:auto">' .. hemi(m1) .. '</div>') .. td('poem-num', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num', 1, nil, '') .. td('b poem-m poem-m2', 5, s2, '<div class="poem-st" style="width:' .. bw .. ';margin-inline-start:auto">' .. hemi(d.m2) .. '</div>') .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end local sp = 'width:' .. d.indent return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, nil, d.r) .. td('b poem-m poem-m1', 3, s1, hemi(m1)) .. td('poem-indent', 2, sp, '') .. td('poem-num', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num', 1, nil, '') .. td('poem-indent', 2, sp, '') .. td('b poem-m poem-m2', 3, s2, hemi(d.m2)) .. td('poem-num poem-num-l', 1, nil, d.l) .. '</tr>' end -- {{مصراع}} local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end -- ترازبندی صریح (مصراع تکنفره هرگز ترازبندی نمیشود مگر خود کاربر بخواهد) local inline = (align == 'right') and ALIGN_STYLE.right or (align == 'left') and ALIGN_STYLE.left or (align == 'justify') and ALIGN_STYLE.justify or ALIGN_STYLE.center local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], nil, d.r) .. td(cellClass, cs[2], style, content) .. td('poem-num poem-num-l', cs[3], nil, d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- ردیف نامرئی نخست در چیدمان ثابت: پهنای ستونها را مستقل از محتوا تعیین میکند local function sizerRow() return '<tr class="poem-sizer">' .. '<td class="poem-s"></td><td></td><td></td>' .. '<td class="poem-g"></td><td></td><td></td>' .. '<td class="poem-s"></td></tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- شعر چندصفحهای: نگهداشتن ردیف ناتمام و یکیکردن آن با دنبالهاش --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end --------------------------------------------------------------------------- -- خواندن پارامترها --------------------------------------------------------------------------- local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil) if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil) if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()] if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style) o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()] if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()] if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g) if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind) if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength(cfg.mesraUnit) if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()] if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w) if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx) if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end --------------------------------------------------------------------------- -- حالت «متن»: ساخت کل شعر از خطهای ساده --------------------------------------------------------------------------- local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*') s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center' s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right' s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v) if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv] if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv] if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') local indent if c.kind == 'stagger' then indent = c.indent or cfg.defaultIndent elseif c.kind == 'ladder' then indent = c.indent or cfg.ladderIndent end return twoH, rowKind, indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1) numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false) pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true) pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end --------------------------------------------------------------------------- -- نقطههای ورود --------------------------------------------------------------------------- local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end if cont and not inPageNs() then return SKIP .. categories(flags) end local html = flushHtml() state.open, state.ctx = false, nil if html ~= '' then html = html .. '\n' end return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()] if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', '' html = rowBayt(d) else d.text, d.blank = '', true html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(cfg.defaultIndent)) p.ladder = safe(staggerFn(cfg.ladderIndent)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p l40i4qf781frueqkrmzlbbkyotf8ink 304022 304011 2026-10-11T21:34:07Z Hanooz 17889 304022 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی الگو نقطهٔ ورود {{شعر}} open {{پایان شعر}} close {{بیت}} bayt {{بیت۱}} stagger {{بیت نردبانی}} ladder {{مصراع}} mesra {{بیت خالی}} emptyBayt {{مصراع خالی}} emptyMesra ▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت: ستون ۱ : شمارهٔ ش (۶٪) ستونهای ۲–۳: مصراع اول (۴۱٫۵٪ مجموع) ستون ۴ : فاصلهٔ میانی شو (۵٪) ستونهای ۵–۶: مصراع دوم (۴۱٫۵٪ مجموع) ستون ۷ : شمارهٔ شچ (۶٪) عرض مصراع در همهٔ انواع (بیت، بیت۱، نردبانی) یکسان است و در همهٔ ابیات یک شعر همتراز میماند. ▸ ترازبندی پیشفرض وسط است. با ترازبندی=پهن یا لبه میتوان دوطرفه یا لبهچین کرد. ▸ صفحهٔ باریک (در CSS زیر ۳۲em): هر مصراع یک خط، با خطچین بین بیتها. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر/styles.css', pageNs = 104, mesraUnit = '1.5em', nbsp = '&#160;', defaultWidth = '100%', maxWidth = '42em', justDefault = { bayt = 'center', stagger = 'center' }, justFallback = 'center', justifyMinRatio = 0.7, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep', } local YES = { ['بله'] = true, ['بلی'] = true, ['آری'] = true, ['خودکار'] = true, yes = true, y = true, auto = true, ['true'] = true, ['1'] = true } local NO = { ['نه'] = true, ['خیر'] = true, no = true, n = true, ['false'] = true, ['0'] = true } local UNITS = { [''] = 'px', ['%'] = '%', px = 'px', em = 'em', ex = 'ex', ch = 'ch', rem = 'rem', vw = 'vw', vh = 'vh', pt = 'pt', cm = 'cm', mm = 'mm', ['in'] = 'in' } local CONT = { ['قبل'] = 'prev', ['پیشین'] = 'prev', prev = 'prev', ['بعد'] = 'next', ['پسین'] = 'next', next = 'next', ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both' } local JOIN = { ['فاصله'] = ' ', space = ' ', ['چسبیده'] = '', ['بدون فاصله'] = '', tight = '', ['نیم' .. ZWNJ .. 'فاصله'] = ZWNJ, ['نیمفاصله'] = ZWNJ, ['نیم فاصله'] = ZWNJ, zwnj = ZWNJ } local WHICH = { ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', ['اول'] = 'first', first = 'first', ['دوم'] = 'second', second = 'second' } local STANZA = { ['جدید'] = true, new = true } local JUST = { ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', ['راست'] = 'right', right = 'right', ['چپ'] = 'left', left = 'left', ['پهن'] = 'justify', ['دوطرفه'] = 'justify', ['دو' .. ZWNJ .. 'طرفه'] = 'justify', justify = 'justify', ['لبه'] = 'edge', edge = 'edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar = true, arz = true, azb = true, bgn = true, ckb = true, dv = true, fa = true, glk = true, he = true, ks = true, lrc = true, mzn = true, pnb = true, prs = true, ps = true, sd = true, ug = true, ur = true, yi = true, } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' --------------------------------------------------------------------------- local function esc(s) s = s:gsub('"', '&quot;'):gsub('<', '&lt;'):gsub('>', '&gt;'); return s end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end s = mw.text.trim(s):gsub('%s*\n%s*', ' ') return s end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn', 1, true) or rest:find('cyrl', 1, true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab', 1, true) or rest:find('hebr', 1, true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0] = '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪', '%%'):gsub('٫', '.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v) if not n then return nil end return string.format('%g', n) .. u end local function newCtx(args, kind, flags) local ctx = { digits = 'fa', count = 0, start = 1, side = 'l', mesra = (kind == 'mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()] if not ctx.digits then ctx.digits = 'fa'; flags.unknown = true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9) if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()] if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function charLen(s) s = s:gsub('\127[^\127]*\127', ''):gsub('<[^>]*>', '') :gsub('%[%[[^%]|]*|', ''):gsub('[%[%]]', ''):gsub('\226\128\140', '') return mw.ustring.len(s) end local function effectiveJust(j, a, b) if j == 'justify' then local la, lb = charLen(a or ''), charLen(b or '') local hi = math.max(la, lb) if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end end return j end local function td(class, colspan, style, content) local s = '<td class="' .. class .. '"' if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemi(t) return '<span class="beyt">' .. t .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end -- شبکهٔ ۷ستونی: ۱ | ۲-۳ (مصراع ۱) | ۴ (میانی) | ۵-۶ (مصراع ۲) | ۷ local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2) return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemi(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end -- بیت۱ و نردبانی — همان شبکهٔ ۷ستونی در دو ردیف local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2) local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemi(m1)) .. td('poem-num poem-num-c', 1, nil, '') .. td('poem-num', 2, nil, '') .. td('poem-num', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num', 1, nil, '') .. td('poem-num', 2, nil, '') .. td('poem-num poem-num-c', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, content) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- سایزر: عرض ستونها را با درصد صریح تعیین میکند، مستقل از محتوا local function sizerRow() return '<tr class="poem-sizer">' .. '<td class="poem-s"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-g"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-s"></td></tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil) if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil) if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()] if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style) o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()] if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength(cfg.mesraUnit) if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end -- (حالت «متن» و توابع دیگر مثل قبل) local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') local indent if c.kind == 'stagger' then indent = c.indent or cfg.defaultIndent elseif c.kind == 'ladder' then indent = c.indent or cfg.ladderIndent end return twoH, rowKind, indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end if cont and not inPageNs() then return SKIP .. categories(flags) end local html = flushHtml() state.open, state.ctx = false, nil if html ~= '' then html = html .. '\n' end return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn('20%')) p.ladder = safe(staggerFn('45%')) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p 60fuv62nazs36il81q7r92p0nhbtrq5 304023 304022 2026-10-11T21:34:38Z Hanooz 17889 304023 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی الگو نقطهٔ ورود {{شعر}} open {{پایان شعر}} close {{بیت}} bayt {{بیت۱}} stagger {{بیت نردبانی}} ladder {{مصراع}} mesra {{بیت خالی}} emptyBayt {{مصراع خالی}} emptyMesra ▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت: ستون ۱ : شمارهٔ ش (۶٪) ستونهای ۲–۳: مصراع اول (۴۱٫۵٪ مجموع) ستون ۴ : فاصلهٔ میانی شو (۵٪) ستونهای ۵–۶: مصراع دوم (۴۱٫۵٪ مجموع) ستون ۷ : شمارهٔ شچ (۶٪) عرض مصراع در همهٔ انواع (بیت، بیت۱، نردبانی) یکسان است و در همهٔ ابیات یک شعر همتراز میماند. ▸ ترازبندی پیشفرض وسط است. با ترازبندی=پهن یا لبه میتوان دوطرفه یا لبهچین کرد. ▸ صفحهٔ باریک (در CSS زیر ۳۲em): هر مصراع یک خط، با خطچین بین بیتها. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, mesraUnit = '1.5em', nbsp = '&#160;', defaultWidth = '100%', maxWidth = '42em', justDefault = { bayt = 'center', stagger = 'center' }, justFallback = 'center', justifyMinRatio = 0.7, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep', } local YES = { ['بله'] = true, ['بلی'] = true, ['آری'] = true, ['خودکار'] = true, yes = true, y = true, auto = true, ['true'] = true, ['1'] = true } local NO = { ['نه'] = true, ['خیر'] = true, no = true, n = true, ['false'] = true, ['0'] = true } local UNITS = { [''] = 'px', ['%'] = '%', px = 'px', em = 'em', ex = 'ex', ch = 'ch', rem = 'rem', vw = 'vw', vh = 'vh', pt = 'pt', cm = 'cm', mm = 'mm', ['in'] = 'in' } local CONT = { ['قبل'] = 'prev', ['پیشین'] = 'prev', prev = 'prev', ['بعد'] = 'next', ['پسین'] = 'next', next = 'next', ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both' } local JOIN = { ['فاصله'] = ' ', space = ' ', ['چسبیده'] = '', ['بدون فاصله'] = '', tight = '', ['نیم' .. ZWNJ .. 'فاصله'] = ZWNJ, ['نیمفاصله'] = ZWNJ, ['نیم فاصله'] = ZWNJ, zwnj = ZWNJ } local WHICH = { ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', ['اول'] = 'first', first = 'first', ['دوم'] = 'second', second = 'second' } local STANZA = { ['جدید'] = true, new = true } local JUST = { ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', ['راست'] = 'right', right = 'right', ['چپ'] = 'left', left = 'left', ['پهن'] = 'justify', ['دوطرفه'] = 'justify', ['دو' .. ZWNJ .. 'طرفه'] = 'justify', justify = 'justify', ['لبه'] = 'edge', edge = 'edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar = true, arz = true, azb = true, bgn = true, ckb = true, dv = true, fa = true, glk = true, he = true, ks = true, lrc = true, mzn = true, pnb = true, prs = true, ps = true, sd = true, ug = true, ur = true, yi = true, } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' --------------------------------------------------------------------------- local function esc(s) s = s:gsub('"', '&quot;'):gsub('<', '&lt;'):gsub('>', '&gt;'); return s end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end s = mw.text.trim(s):gsub('%s*\n%s*', ' ') return s end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn', 1, true) or rest:find('cyrl', 1, true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab', 1, true) or rest:find('hebr', 1, true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0] = '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪', '%%'):gsub('٫', '.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v) if not n then return nil end return string.format('%g', n) .. u end local function newCtx(args, kind, flags) local ctx = { digits = 'fa', count = 0, start = 1, side = 'l', mesra = (kind == 'mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()] if not ctx.digits then ctx.digits = 'fa'; flags.unknown = true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9) if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()] if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function charLen(s) s = s:gsub('\127[^\127]*\127', ''):gsub('<[^>]*>', '') :gsub('%[%[[^%]|]*|', ''):gsub('[%[%]]', ''):gsub('\226\128\140', '') return mw.ustring.len(s) end local function effectiveJust(j, a, b) if j == 'justify' then local la, lb = charLen(a or ''), charLen(b or '') local hi = math.max(la, lb) if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end end return j end local function td(class, colspan, style, content) local s = '<td class="' .. class .. '"' if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemi(t) return '<span class="beyt">' .. t .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end -- شبکهٔ ۷ستونی: ۱ | ۲-۳ (مصراع ۱) | ۴ (میانی) | ۵-۶ (مصراع ۲) | ۷ local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2) return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemi(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end -- بیت۱ و نردبانی — همان شبکهٔ ۷ستونی در دو ردیف local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2) local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemi(m1)) .. td('poem-num poem-num-c', 1, nil, '') .. td('poem-num', 2, nil, '') .. td('poem-num', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num', 1, nil, '') .. td('poem-num', 2, nil, '') .. td('poem-num poem-num-c', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, content) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- سایزر: عرض ستونها را با درصد صریح تعیین میکند، مستقل از محتوا local function sizerRow() return '<tr class="poem-sizer">' .. '<td class="poem-s"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-g"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-s"></td></tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil) if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil) if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()] if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style) o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()] if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength(cfg.mesraUnit) if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end -- (حالت «متن» و توابع دیگر مثل قبل) local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') local indent if c.kind == 'stagger' then indent = c.indent or cfg.defaultIndent elseif c.kind == 'ladder' then indent = c.indent or cfg.ladderIndent end return twoH, rowKind, indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end if cont and not inPageNs() then return SKIP .. categories(flags) end local html = flushHtml() state.open, state.ctx = false, nil if html ~= '' then html = html .. '\n' end return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn('20%')) p.ladder = safe(staggerFn('45%')) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p o0bnt55y5ei3rau50ny9w9pq1ov29tg 304024 304023 2026-10-11T21:49:23Z Hanooz 17889 304024 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی همهٔ مصراعهای یک شعر — در همهٔ انواع (بیت، بیت۱، نردبانی) — هماندازهاند و اندازهٔ آنها برابر بلندترین مصراع آن شعر است. برای این کار ردیفِ سایزرِ نامرئی در پایان جدول تولید میشود که متن بلندترین مصراع (پس از حذف ارجاعهای پایانی) را در هر دو ستون مصراع میگذارد. با table-layout: auto مرورگر عرض هر دو ستون مصراع را برابر همان متن میکند. ارجاعهای پایانی (<ref> در پایان مصراع) در سنجش بلندی شمرده نمیشوند. صفحهٔ باریک (CSS زیر ۳۲em): هر مصراع یک خط، با خطچین بین بیتها. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '42em', justDefault = { bayt = 'center', stagger = 'center' }, justFallback = 'center', justifyMinRatio = 0.7, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله'] = true, ['بلی'] = true, ['آری'] = true, ['خودکار'] = true, yes = true, y = true, auto = true, ['true'] = true, ['1'] = true } local NO = { ['نه'] = true, ['خیر'] = true, no = true, n = true, ['false'] = true, ['0'] = true } local UNITS = { [''] = 'px', ['%'] = '%', px = 'px', em = 'em', ex = 'ex', ch = 'ch', rem = 'rem', vw = 'vw', vh = 'vh', pt = 'pt', cm = 'cm', mm = 'mm', ['in'] = 'in' } local CONT = { ['قبل'] = 'prev', ['پیشین'] = 'prev', prev = 'prev', ['بعد'] = 'next', ['پسین'] = 'next', next = 'next', ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both' } local JOIN = { ['فاصله'] = ' ', space = ' ', ['چسبیده'] = '', ['بدون فاصله'] = '', tight = '', ['نیم' .. ZWNJ .. 'فاصله'] = ZWNJ, ['نیمفاصله'] = ZWNJ, ['نیم فاصله'] = ZWNJ, zwnj = ZWNJ } local WHICH = { ['هر دو'] = 'both', ['هردو'] = 'both', both = 'both', ['اول'] = 'first', first = 'first', ['دوم'] = 'second', second = 'second' } local STANZA = { ['جدید'] = true, new = true } local JUST = { ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', ['راست'] = 'right', right = 'right', ['چپ'] = 'left', left = 'left', ['پهن'] = 'justify', ['دوطرفه'] = 'justify', ['دو' .. ZWNJ .. 'طرفه'] = 'justify', justify = 'justify', ['لبه'] = 'edge', edge = 'edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil, hemistichs = {} } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' -- Helpers local function esc(s) return (s:gsub('"', '&quot;'):gsub('<', '&lt;'):gsub('>', '&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn', 1, true) or rest:find('cyrl', 1, true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab', 1, true) or rest:find('hebr', 1, true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0] = '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪', '%%'):gsub('٫', '.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end --------------------------------------------------------------------------- -- سنجش بلندی و متن سایزر --------------------------------------------------------------------------- -- حذف <ref>...</ref> پایانی (و پیوندها در متن) local function stripEndRefs(s) if not s then return '' end local prev repeat prev = s s = s:gsub('%s*<ref[^>]*>.-</ref>%s*$', '') s = s:gsub('%s*<ref[^>]*/>%s*$', '') s = s:gsub('%s*\127[^\127]*\127%s*$', '') -- strip marker until s == prev return s end -- فقط متنِ خالص: بدون تگ HTML و ویکیمتن ساده local function plainText(s) if not s or s == '' then return '' end s = s:gsub('<[^>]*>', '') s = s:gsub('%[%[[^%]]*|([^%]]*)%]%]', '%1') s = s:gsub('%[%[([^%]]*)%]%]', '%1') s = s:gsub('{{[^}]*}}', '') s = s:gsub("'''''", ''):gsub("'''", ''):gsub("''", '') return s end local function findLongest() local longest, longestLen = '', 0 for _, h in ipairs(state.hemistichs) do local s = plainText(stripEndRefs(h)) local len = mw.ustring.len(s) if len > longestLen then longest, longestLen = s, len end end return longest end -- ردیف سایزر: متن بلندترین مصراع در هر دو ستون مصراع، نامرئی، با ارتفاع صفر local function sizerRow() local longest = findLongest() if longest == '' then return '' end local cell = '<span class="poem-sz">' .. esc(longest) .. '</span>' return '<tr class="poem-sizer" aria-hidden="true">' .. '<td class="poem-s"></td>' .. '<td colspan="2" class="poem-mh">' .. cell .. '</td>' .. '<td class="poem-g"></td>' .. '<td colspan="2" class="poem-mh">' .. cell .. '</td>' .. '<td class="poem-s"></td>' .. '</tr>' end --------------------------------------------------------------------------- -- شمارهگذاری --------------------------------------------------------------------------- local function newCtx(args, kind, flags) local ctx = { digits = 'fa', count = 0, start = 1, side = 'l', mesra = (kind == 'mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits = 'fa'; flags.unknown = true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end --------------------------------------------------------------------------- -- ساخت HTML --------------------------------------------------------------------------- local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function charLen(s) s = s:gsub('\127[^\127]*\127', ''):gsub('<[^>]*>', '') :gsub('%[%[[^%]|]*|', ''):gsub('[%[%]]', ''):gsub('\226\128\140', '') return mw.ustring.len(s) end local function effectiveJust(j, a, b) if j == 'justify' then local la, lb = charLen(a or ''), charLen(b or '') local hi = math.max(la, lb) if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end end return j end local function td(class, colspan, style, content) local s = '<td class="' .. class .. '"' if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemi(t) return '<span class="beyt">' .. t .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end -- ثبت مصراع برای سنجش local function register(m1, m2) if not state.open then return end if m1 and m1 ~= '' then state.hemistichs[#state.hemistichs + 1] = m1 end if m2 and m2 ~= '' then state.hemistichs[#state.hemistichs + 1] = m2 end end -- {{بیت}} — شبکهٔ ۷ستونی: ۱ | ۲-۳ (م۱) | ۴ | ۵-۶ (م۲) | ۷ local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end register(d.m1, d.m2) local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2) return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemi(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end -- {{بیت۱}} و {{بیت نردبانی}} — دو ردیف در همان شبکهٔ ۷ستونی local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end register(d.m1, d.m2) local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2) local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemi(m1)) .. td('poem-num poem-num-c', 1, nil, '') .. td('b poem-m poem-m-empty', 2, nil, '') .. td('poem-num poem-num-l', 1, 'text-align:right', '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r', 1, 'text-align:left', '') .. td('b poem-m poem-m-empty', 2, nil, '') .. td('poem-num poem-num-c', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end -- {{مصراع}} local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end if not d.blank then register(d.text, nil) end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, content) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation">' end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- شعر چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end --------------------------------------------------------------------------- -- خواندن پارامترها --------------------------------------------------------------------------- local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end -- حالت متن (خلاصه: همان منطق پیشین) local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') local indent if c.kind == 'stagger' then indent = c.indent or '20%' elseif c.kind == 'ladder' then indent = c.indent or '45%' end return twoH, rowKind, indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end --------------------------------------------------------------------------- -- نقطههای ورود --------------------------------------------------------------------------- local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil state.hemistichs = {} end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = sizerRow() parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil state.hemistichs = {} end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) if not state.open then -- اگر متن استفاده شده و داخل open بسته شده، اینجا چیزی نمیفرستیم local args, flags = getArgs(frame), {} if hasUnknown(args, 1) then flags.unknown = true end return SKIP .. categories(flags) end local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end if cont and not inPageNs() then return SKIP .. categories(flags) end local html = flushHtml() state.open, state.ctx = false, nil local sz = sizerRow() state.hemistichs = {} if html ~= '' then html = html .. '\n' end return html .. sz .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn('20%')) p.ladder = safe(staggerFn('45%')) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p a893f0u1c8yh5dqqkbh9cu3gheltzkf 304027 304024 2026-10-11T22:07:14Z Hanooz 17889 304027 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت: ۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ پس مصراعها در همهٔ ابیات و همهٔ انواع هماندازهاند. ▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تعیین میکند. ▸ ترازبندی پیشفرض وسط است. «پهن» و «لبه» اختیاری. ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. بیت۱ دو ردیف است، پس خطچین فقط زیر ردیف دوم میآید. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '42em', justDefault = { bayt = 'center', stagger = 'center' }, justFallback = 'center', justifyMinRatio = 0.7, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local function esc(s) return (s:gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function charLen(s) s = s:gsub('\127[^\127]*\127',''):gsub('<[^>]*>','') :gsub('%[%[[^%]|]*|',''):gsub('[%[%]]',''):gsub('\226\128\140','') return mw.ustring.len(s) end local function effectiveJust(j, a, b) if j == 'justify' then local la, lb = charLen(a or ''), charLen(b or '') local hi = math.max(la, lb) if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end end return j end local function td(class, colspan, style, content) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemi(t) return '<span class="beyt">' .. t .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end -- {{بیت}} — 7 cells local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2) return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemi(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end -- {{بیت۱}} و {{بیت نردبانی}} — دو ردیف، همان شبکهٔ ۷ستونی local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2) local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end -- ردیف ۱: num-r | m1 | gap | empty | empty -- ردیف ۲: empty | empty | gap | m2 | num-l return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemi(m1)) .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('poem-empty poem-mh-empty', 2, nil, '') .. td('poem-num poem-num-l poem-empty', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '') .. td('poem-empty poem-mh-empty', 2, nil, '') .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, content) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- سایزرِ خالی: عرض ستونها را در table-layout: fixed تثبیت میکند. local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. '<td class="poem-s"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-g"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-s"></td>' .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- شعر چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end -- حالت متن (خلاصه) local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') local indent if c.kind == 'stagger' then indent = c.indent or '20%' elseif c.kind == 'ladder' then indent = c.indent or '45%' end return twoH, rowKind, indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end if cont and not inPageNs() then return SKIP .. categories(flags) end local html = flushHtml() state.open, state.ctx = false, nil if html ~= '' then html = html .. '\n' end return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn('20%')) p.ladder = safe(staggerFn('45%')) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p 8trfjqv80npnu91vtm40fbb1ikdjrb6 304028 304027 2026-10-11T22:08:18Z Hanooz 17889 304028 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت: ۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ پس مصراعها در همهٔ ابیات و همهٔ انواع هماندازهاند. ▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تعیین میکند. ▸ ترازبندی پیشفرض وسط است. «پهن» و «لبه» اختیاری. ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. بیت۱ دو ردیف است، پس خطچین فقط زیر ردیف دوم میآید. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '42em', justDefault = { bayt = 'center', stagger = 'center' }, justFallback = 'center', justifyMinRatio = 0.7, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local function esc(s) return (s:gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function charLen(s) s = s:gsub('\127[^\127]*\127',''):gsub('<[^>]*>','') :gsub('%[%[[^%]|]*|',''):gsub('[%[%]]',''):gsub('\226\128\140','') return mw.ustring.len(s) end local function effectiveJust(j, a, b) if j == 'justify' then local la, lb = charLen(a or ''), charLen(b or '') local hi = math.max(la, lb) if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end end return j end local function td(class, colspan, style, content) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemi(t) return '<span class="beyt">' .. t .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end -- {{بیت}} — 7 cells local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2) return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemi(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end -- {{بیت۱}} و {{بیت نردبانی}} — دو ردیف، همان شبکهٔ ۷ستونی local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2) local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end -- ردیف ۱: num-r | m1 | gap | empty | empty -- ردیف ۲: empty | empty | gap | m2 | num-l return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemi(m1)) .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('poem-empty poem-mh-empty', 2, nil, '') .. td('poem-num poem-num-l poem-empty', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '') .. td('poem-empty poem-mh-empty', 2, nil, '') .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, content) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- سایزرِ خالی: عرض ستونها را در table-layout: fixed تثبیت میکند. local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. '<td class="poem-s"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-g"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-s"></td>' .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- شعر چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') and (o.just ~= 'justify') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end -- حالت متن (خلاصه) local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') local indent if c.kind == 'stagger' then indent = c.indent or '20%' elseif c.kind == 'ladder' then indent = c.indent or '45%' end return twoH, rowKind, indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end if cont and not inPageNs() then return SKIP .. categories(flags) end local html = flushHtml() state.open, state.ctx = false, nil if html ~= '' then html = html .. '\n' end return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn('20%')) p.ladder = safe(staggerFn('45%')) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p lreao07tijsr68j7gdwrqqol4hj3gy9 304030 304028 2026-10-11T22:22:08Z Hanooz 17889 304030 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت: ۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ پس مصراعها در همهٔ ابیات و همهٔ انواع (بیت، بیت۱، نردبانی) هماندازهاند. ▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تعیین میکند. ▸ ترازبندی پیشفرض وسط است. «پهن» و «لبه» اختیاری. ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. بیت۱ دو ردیف است، پس خطچین فقط زیر ردیف دوم میآید. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '42em', justDefault = { bayt = 'center', stagger = 'center' }, justFallback = 'center', justifyMinRatio = 0.7, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local function esc(s) return (s:gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function charLen(s) s = s:gsub('\127[^\127]*\127',''):gsub('<[^>]*>','') :gsub('%[%[[^%]|]*|',''):gsub('[%[%]]',''):gsub('\226\128\140','') return mw.ustring.len(s) end local function effectiveJust(j, a, b) if j == 'justify' then local la, lb = charLen(a or ''), charLen(b or '') local hi = math.max(la, lb) if hi > 0 and math.min(la, lb) / hi < cfg.justifyMinRatio then return cfg.justFallback end end return j end local function td(class, colspan, style, content) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end local function hemi(t) return '<span class="beyt">' .. t .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end -- {{بیت}} — 7 cells local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.bayt, d.m1, d.m2) return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemi(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end -- {{بیت۱}} و {{بیت نردبانی}} — دو ردیف، همان شبکهٔ ۷ستونی local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.stagger, d.m1, d.m2) local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemi(m1)) .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-l poem-empty', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemi(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, content) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } -- سایزرِ خالی: عرض ستونها را در table-layout: fixed تثبیت میکند. local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. '<td class="poem-s"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-g"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-s"></td>' .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- شعر چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end -- حالت متن local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') local indent if c.kind == 'stagger' then indent = c.indent elseif c.kind == 'ladder' then indent = c.indent end return twoH, rowKind, indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end if cont and not inPageNs() then return SKIP .. categories(flags) end local html = flushHtml() state.open, state.ctx = false, nil if html ~= '' then html = html .. '\n' end return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if raw then d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p oz14oetj7cnzfwt95b38bq2gcdy5z69 304032 304030 2026-10-11T22:33:40Z Hanooz 17889 304032 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت: ۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ پس مصراعها در همهٔ ابیات و همهٔ انواع هماندازهاند. ▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تعیین میکند. ▸ ترازبندی پیشفرض وسط است. با ترازبندی=پهن دوطرفه و با لبه، مصراع اول به لبهٔ راست و دوم به لبهٔ چپ میرود. «پهن» همیشه کشیده میکند و به هیچ تراز دیگری فرو نمیافتد. ▸ ارجاعهای پایانی (<ref>) در سنجش بلندی شمرده نمیشوند و در خروجی، پس از متن کشیدهشده و در انتهای مصراع قرار میگیرند. ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '42em', justDefault = { bayt = 'center', stagger = 'center' }, justFallback = 'center', nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local FINAL_MARKER = '<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>' local function esc(s) return (s:gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end --------------------------------------------------------------------------- -- ارجاعها: جداسازی و حذف از متن (پیش از سنجش بلندی) --------------------------------------------------------------------------- -- حذف همهٔ <ref>...</ref>، <ref/>، و نشانگرهای \127...\127 از یک رشته local function stripRefs(s) if not s or s == '' then return '' end local prev repeat prev = s s = s:gsub('<ref[^>]*%s*/>', '') s = s:gsub('<ref[^>]*>.-</ref>', '') s = s:gsub('\127[^\127]*\127', '') until s == prev return s end -- استخراج دنبالهٔ <ref>ها و نشانگرهای \127 از پایان رشته. -- بازگردانده: (متنِ بدون ارجاعهای پایانی، رشتهٔ ارجاعها) local function splitTrailingRefs(s) if not s or s == '' then return '', '' end -- ناحیهٔ ارجاعها را از انتها جدا کن local refs = {} local text = s local changed = true while changed do changed = false -- <ref ...>...</ref> در انتها local before, ref, after = text:match('^(.-)(<ref[^>]*>.-</ref>)%s*$') if before then refs[#refs + 1] = ref text = before changed = true else -- <ref .../> در انتها local b2, r2 = text:match('^(.-)(<ref[^>]*%s*/>)%s*$') if b2 then refs[#refs + 1] = r2 text = b2 changed = true else -- نشانگر \127...\127 در انتها local b3, r3 = text:match('^(.-)(\127[^\127]*\127)%s*$') if b3 then refs[#refs + 1] = r3 text = b3 changed = true end end end end -- ترتیب معکوس (از آخر به اول جمع شده) local ordered = {} for i = #refs, 1, -1 do ordered[#ordered + 1] = refs[i] end return mw.text.trim(text), table.concat(ordered) end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end -- طول فقط متن دیدنی: ارجاعها، تگها، پیشوند پیوند و نیمفاصله حذف میشوند local function charLen(s) s = stripRefs(s) s = s:gsub('<[^>]*>','') :gsub('%[%[[^%]|]*|','') :gsub('[%[%]]','') :gsub('\226\128\140','') return mw.ustring.len(s) end -- ترازبندی «پهن» همیشه اعمال میشود؛ هیچ فروافتادی نیست. local function effectiveJust(j) return j end local function td(class, colspan, style, content) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end -- محتوای یک مصراع: متنِ کشیدهشده + ارجاعهای پایانی local function hemiCell(rawText) local body, refs = splitTrailingRefs(rawText or '') local inner if refs ~= '' then inner = '<span class="beyt"><span class="beyt-text">' .. body .. '</span><span class="beyt-refs">' .. refs .. '</span></span>' else inner = '<span class="beyt">' .. body .. '</span>' end return inner end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.bayt) return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = effectiveJust(d.just or cfg.justDefault.stagger) local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemiCell(m1)) .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-l poem-empty', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end local cell if d.blank and content == cfg.nbsp then cell = content else cell = hemiCell(content) end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, cell) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. '<td class="poem-s"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-g"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-s"></td>' .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- شعر چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') return twoH, rowKind, c.indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil else parts[#parts + 1] = FINAL_MARKER end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end local html = flushHtml() if html ~= '' then html = html .. '\n' end if cont and not inPageNs() then return html .. FINAL_MARKER .. categories(flags) end state.open, state.ctx = false, nil return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) if raw then d.indent = parseLength(raw) if not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p iy28ioz1pt6go0s048txpp71to2wwyr 304035 304032 2026-10-11T22:44:52Z Hanooz 17889 304035 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت: ۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ ▸ ترازبندی پیشفرض «پهن» (دوطرفه) است. با ترازبندی=وسط/راست/چپ/لبه قابل تغییر است. ▸ ارجاعهای پایانی (<ref>) از متن جدا میشوند و در ستون کوچک انتهای مصراع مینشینند؛ متن مصراع مستقل از آنها کشیده میشود و کوتاه بهنظر نمیرسد. ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '42em', justDefault = { bayt = 'justify', stagger = 'center' }, justFallback = 'center', nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local FINAL_MARKER = '<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>' local function esc(s) return (s:gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end --------------------------------------------------------------------------- -- ارجاعها --------------------------------------------------------------------------- local function stripRefs(s) if not s or s == '' then return '' end local prev repeat prev = s s = s:gsub('<ref[^>]*%s*/>', '') s = s:gsub('<ref[^>]*>.-</ref>', '') s = s:gsub('\127[^\127]*\127', '') until s == prev return s end -- جداسازی دنبالهٔ ارجاعها از متن؛ برمیگرداند (متن، ارجاعها) local function splitTrailingRefs(s) if not s or s == '' then return '', '' end local refs = {} local text = s local changed = true while changed do changed = false local before, ref before, ref = text:match('^(.-)(<ref[^>]*>.-</ref>)%s*$') if not before then before, ref = text:match('^(.-)(<ref[^>]*%s*/>)%s*$') end if not before then before, ref = text:match('^(.-)(\127[^\127]*\127)%s*$') end if before then refs[#refs + 1] = ref text = before changed = true end end local ordered = {} for i = #refs, 1, -1 do ordered[#ordered + 1] = refs[i] end return mw.text.trim(text), table.concat(ordered) end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end local function charLen(s) s = stripRefs(s) s = s:gsub('<[^>]*>','') :gsub('%[%[[^%]|]*|','') :gsub('[%[%]]','') :gsub('\226\128\140','') return mw.ustring.len(s) end local function td(class, colspan, style, content) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end -- محتوای مصراع: متن در flex:1 ، ارجاعها در flex:0 local function hemiCell(rawText) local body, refs = splitTrailingRefs(rawText or '') local inner = '<span class="beyt"><span class="beyt-text">' .. body .. '</span>' if refs ~= '' then inner = inner .. '<span class="beyt-refs">' .. refs .. '</span>' end return inner .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.bayt return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.stagger local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemiCell(m1)) .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-l poem-empty', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end local cell if d.blank and content == cfg.nbsp then cell = content else cell = hemiCell(content) end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, cell) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. '<td class="poem-s"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-g"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-s"></td>' .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- شعر چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') return twoH, rowKind, c.indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil else parts[#parts + 1] = FINAL_MARKER end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end local html = flushHtml() if html ~= '' then html = html .. '\n' end if cont and not inPageNs() then return html .. FINAL_MARKER .. categories(flags) end state.open, state.ctx = false, nil return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) if raw then d.indent = parseLength(raw) if not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p k57uxfk0yj1ebmtgys6dkahmqvbntzt 304037 304035 2026-10-12T03:59:36Z Hanooz 17889 304037 Scribunto text/plain --[==[ پودمان:شعر — نمایش شعر در ویکینبشتهٔ فارسی ▸ همهٔ انواع بیت روی یک شبکهٔ ۷ستونی با عرضهای درصدی ثابت: ۱ ش | ۲-۳ مصراع اول | ۴ شو | ۵-۶ مصراع دوم | ۷ شچ ▸ ردیف سایزرِ خالی در آغاز جدول، عرض ستونها را تعیین میکند. ▸ ترازبندی پیشفرض «وسط». با ترازبندی=پهن دوطرفه و با لبه، مصراع اول به لبهٔ راست و دوم به لبهٔ چپ میرود. ▸ ارجاعهای پایانی (<ref>) در سنجش بلندی شمرده نمیشوند اما در متن بهصورت inline میمانند و به آخرین واژه میچسبند. بهاینترتیب حضور ارجاع، طولِ قابل کشیدنِ متن را کوتاه نمیکند و فقط چند پیکسل فضای طبیعی خودش را میگیرد. ▸ صفحهٔ باریک (< ۳۲em): هر مصراع یک خط، خطچین پس از هر بیت. ]==] local p = {} local ZWNJ = '\226\128\140' local cfg = { styles = 'Template:شعر۲/styles.css', pageNs = 104, nbsp = '&#160;', defaultWidth = '100%', maxWidth = '42em', justDefault = { bayt = 'center', stagger = 'center' }, nastaliqFonts = "'IranNastaliq', 'Nastaliq Navees', 'Noto Nastaliq Urdu', " .. "'Jameel Noori Nastaleeq', 'Nafees Nastaleeq', 'Pak Nastaleeq', serif", mergeRows = false, cats = { unknown = 'صفحه' .. ZWNJ .. 'های دارای پارامتر ناشناخته در الگوهای شعر', incomplete = 'صفحه' .. ZWNJ .. 'های دارای بیت ناقص', error = 'صفحه' .. ZWNJ .. 'های دارای خطا در پودمان شعر', }, } local N = { right = { 'ش', 'r', 'right' }, center = { 'شو', 'c', 'center' }, left = { 'شچ', 'l', 'left' }, style = { 'سبک', 'style' }, class = { 'کلاس', 'class' }, id = { 'شناسه', 'id' }, indent = { 'تورفتگی', 'indent' }, align = { 'تراز', 'align' }, height = { 'ارتفاع', 'height' }, lang = { 'زبان', 'lang' }, dir = { 'جهت', 'dir' }, text = { 'متن', 'text' }, kind = { 'نوع', 'type' }, numbering = { 'شماره' .. ZWNJ .. 'گذاری', 'شمارهگذاری', 'شماره', 'numbering' }, start = { 'شروع', 'start' }, side = { 'سمت', 'side' }, digits = { 'ارقام', 'digits' }, gap = { 'فاصله', 'gap' }, unit = { 'واحد تورفتگی', 'واحد' .. ZWNJ .. 'تورفتگی', 'واحدتورفتگی', 'indent-unit' }, nastaliq = { 'نستعلیق', 'nastaliq' }, cont = { 'ادامه', 'continue' }, unfinished = { 'ناتمام', 'unfinished' }, continued = { 'دنباله', 'continued' }, joiner = { 'اتصال', 'join' }, stanza = { 'بند', 'stanza' }, width = { 'عرض', 'width' }, maxwidth = { 'حداکثر عرض', 'حداکثر' .. ZWNJ .. 'عرض', 'حداکثرعرض', 'max-width' }, font = { 'قلم', 'font' }, just = { 'ترازبندی', 'تراز' .. ZWNJ .. 'بندی', 'justify' }, } local KNOWN = {} for _, list in pairs(N) do for _, name in ipairs(list) do KNOWN[name] = true end end local ALIGN = { ['راست'] = 'right', right = 'right', r = 'right', ['چپ'] = 'left', left = 'left', l = 'left', ['وسط'] = 'center', ['مرکز'] = 'center', center = 'center', c = 'center', } local SIDE = { right = 'r', left = 'l', center = 'c' } local KIND = { ['بیت'] = 'bayt', ['ابیات'] = 'bayt', bayt = 'bayt', ['بیت۱'] = 'stagger', ['بیت1'] = 'stagger', ['پلکانی'] = 'stagger', ['ضربدری'] = 'stagger', stagger = 'stagger', ['بیت نردبانی'] = 'ladder', ['نردبانی'] = 'ladder', ladder = 'ladder', ['مصراع'] = 'mesra', ['نو'] = 'mesra', ['نیمایی'] = 'mesra', ['آزاد'] = 'mesra', mesra = 'mesra', line = 'mesra', } local DIGITS = { ['فارسی'] = 'fa', fa = 'fa', persian = 'fa', ['لاتین'] = 'latin', ['انگلیسی'] = 'latin', latin = 'latin', en = 'latin', ['بدون' .. ZWNJ .. 'تغییر'] = 'keep', ['بدون تغییر'] = 'keep', keep = 'keep' } local YES = { ['بله']=true, ['بلی']=true, ['آری']=true, ['خودکار']=true, yes=true, y=true, auto=true, ['true']=true, ['1']=true } local NO = { ['نه']=true, ['خیر']=true, no=true, n=true, ['false']=true, ['0']=true } local UNITS = { ['']='px', ['%']='%', px='px', em='em', ex='ex', ch='ch', rem='rem', vw='vw', vh='vh', pt='pt', cm='cm', mm='mm', ['in']='in' } local CONT = { ['قبل']='prev', ['پیشین']='prev', prev='prev', ['بعد']='next', ['پسین']='next', next='next', ['هر دو']='both', ['هردو']='both', both='both' } local JOIN = { ['فاصله']=' ', space=' ', ['چسبیده']='', ['بدون فاصله']='', tight='', ['نیم' .. ZWNJ .. 'فاصله']=ZWNJ, ['نیمفاصله']=ZWNJ, ['نیم فاصله']=ZWNJ, zwnj=ZWNJ } local WHICH = { ['هر دو']='both', ['هردو']='both', both='both', ['اول']='first', first='first', ['دوم']='second', second='second' } local STANZA = { ['جدید']=true, new=true } local JUST = { ['وسط']='center', ['مرکز']='center', center='center', ['راست']='right', right='right', ['چپ']='left', left='left', ['پهن']='justify', ['دوطرفه']='justify', ['دو' .. ZWNJ .. 'طرفه']='justify', justify='justify', ['لبه']='edge', edge='edge', } local ALIGN_STYLE = { right = 'text-align:right;text-align-last:auto', left = 'text-align:left;text-align-last:auto', center = 'text-align:center;text-align-last:auto', justify = 'text-align:justify;text-align-last:justify;' .. '-moz-text-align-last:justify;-webkit-text-align-last:justify', } local KIND_KEYS = {} for k in pairs(KIND) do KIND_KEYS[#KIND_KEYS + 1] = k end table.sort(KIND_KEYS, function(a, b) return #a > #b end) local RTL_LANGS = { ar=1, arz=1, azb=1, bgn=1, ckb=1, dv=1, fa=1, glk=1, he=1, ks=1, lrc=1, mzn=1, pnb=1, prs=1, ps=1, sd=1, ug=1, ur=1, yi=1 } local state = { ctx = nil, open = false, carry = nil } local function defaultCtx() return { digits = 'fa', fixed = (cfg.defaultWidth ~= 'auto') } end local SKIP = '<tr class="poem-skip"></tr>' local CLOSE_HTML = '</table></div>' local FINAL_MARKER = '<tr class="poem-row poem-final-marker" aria-hidden="true"><td colspan="7"></td></tr>' local function esc(s) return (s:gsub('"','&quot;'):gsub('<','&lt;'):gsub('>','&gt;')) end local function pick(args, names) for _, n in ipairs(names) do if args[n] and args[n] ~= '' then return args[n] end end return nil end local function present(args, names) for _, n in ipairs(names) do if args[n] ~= nil then return true end end return false end local function bool(v, d) if v == nil then return d end v = v:lower() if YES[v] then return true end if NO[v] then return false end return d end local function addClass(a, b) if not a or a == '' then return b end return a .. ' ' .. b end local function clean(s) if not s then return '' end return (mw.text.trim(s):gsub('%s*\n%s*', ' ')) end local function getArgs(frame) local a = {} local function merge(src) if not src then return end for k, v in pairs(src) do if type(v) == 'string' then a[k] = mw.text.trim(v) end end end merge(frame.args) local parent = frame:getParent() if parent then merge(parent.args) end return a end local function hasUnknown(args, pos) for k, v in pairs(args) do if v ~= '' then if type(k) == 'number' then if k < 1 or k > pos or k % 1 ~= 0 then return true end elseif not KNOWN[k] then return true end end end return false end local function nsId() return mw.title.getCurrentTitle().namespace end local function inPageNs() return nsId() == cfg.pageNs end local function tracked() local ns = nsId() return ns == 0 or (ns >= 100 and ns % 2 == 0 and ns ~= 828) end local function categories(flags) if not flags or next(flags) == nil or not tracked() then return '' end local out = {} for _, key in ipairs({ 'unknown', 'incomplete', 'error' }) do if flags[key] then out[#out + 1] = '[[رده:' .. cfg.cats[key] .. ']]' end end return table.concat(out) end local function langDir(lang) local base, rest = lang:lower():match('^(%a+)(.*)$') if not base then return nil end if rest:find('latn',1,true) or rest:find('cyrl',1,true) then return 'ltr' end if RTL_LANGS[base] or rest:find('arab',1,true) or rest:find('hebr',1,true) then return 'rtl' end return 'ltr' end local function stylesTag(frame) local ok, tag = pcall(function() return frame:extensionTag{ name = 'templatestyles', args = { src = cfg.styles } } end) return ok and tag or '' end local FA = { [0]='۰','۱','۲','۳','۴','۵','۶','۷','۸','۹' } local function convertDigits(s, mode) if s == '' or mode == 'keep' then return s end if s:find('[<>{}&=|\127]') or s:find('%[%[') then return s end if mode == 'latin' then s = s:gsub('\217([\160-\169])', function(b) return tostring(b:byte() - 160) end) s = s:gsub('\219([\176-\185])', function(b) return tostring(b:byte() - 176) end) return s end s = s:gsub('%d', function(d) return FA[tonumber(d)] end) s = s:gsub('\217([\160-\169])', function(b) return FA[b:byte() - 160] end) return s end local function numberArg(args, names, digits) local v = pick(args, names); if not v then return '' end return convertDigits(v, digits) end local function intArg(v, lo, hi) local n = tonumber((convertDigits(v, 'latin'))) if not n or n ~= n or n < lo or n > hi then return nil end return math.floor(n) end local function splitLength(v) if not v or v == '' then return nil end v = convertDigits(v, 'latin'):gsub('٪','%%'):gsub('٫','.') local num, unit = v:match('^(%d*%.?%d+)%s*([%a%%]*)$') if not num then return nil end num = tonumber(num) if not num or num > 10000 then return nil end unit = UNITS[unit:lower()] if not unit then return nil end return num, unit end local function parseLength(v) local n, u = splitLength(v); if not n then return nil end return string.format('%g', n) .. u end --------------------------------------------------------------------------- -- ارجاعها فقط برای سنجش بلندی حذف میشوند، نه برای نمایش --------------------------------------------------------------------------- local function stripRefs(s) if not s or s == '' then return '' end local prev repeat prev = s s = s:gsub('<ref[^>]*%s*/>', '') s = s:gsub('<ref[^>]*>.-</ref>', '') s = s:gsub('\127[^\127]*\127', '') until s == prev return s end local function newCtx(args, kind, flags) local ctx = { digits='fa', count=0, start=1, side='l', mesra=(kind=='mesra') } local d = pick(args, N.digits) if d then ctx.digits = DIGITS[d:lower()]; if not ctx.digits then ctx.digits='fa'; flags.unknown=true end end local s = pick(args, N.start) if s then local n = intArg(s, -1e9, 1e9); if n then ctx.start = n else flags.unknown = true end end local sd = pick(args, N.side) if sd then local a = ALIGN[sd:lower()]; if a then ctx.side = SIDE[a] else flags.unknown = true end end local m = pick(args, N.numbering) if m then local n = intArg(m, 0, 1e6) if n then if n >= 1 then ctx.every = n end else local b = bool(m, nil) if b == true then ctx.every = 1 elseif b == nil then flags.unknown = true end end end return ctx end local function nextNumber(ctx) if not ctx.every then return nil end local n = ctx.start + ctx.count ctx.count = ctx.count + 1 if n % ctx.every ~= 0 then return nil end return convertDigits(string.format('%d', n), ctx.digits) end local function applyAuto(d, ctx, counted, noCenter) if not counted then return end local n = nextNumber(ctx) if not n then return end if d.r ~= '' or d.c ~= '' or d.l ~= '' then return end local side = ctx.side if noCenter and side == 'c' then side = 'l' end d[side] = n end local function joinStyle(...) local parts = {} for i = 1, select('#', ...) do local v = select(i, ...) if v and v ~= '' then parts[#parts + 1] = (v:gsub(';%s*$', '')) end end return table.concat(parts, ';') end local function alignFor(j, which) if not j or j == 'center' then return nil end if j == 'edge' then return (which == 1) and 'text-align:start;text-align-last:auto' or 'text-align:end;text-align-last:auto' end return ALIGN_STYLE[j] end -- طول فقط متن دیدنی: ارجاعها، تگها، پیشوند پیوند و نیمفاصله حذف میشوند local function charLen(s) s = stripRefs(s) s = s:gsub('<[^>]*>','') :gsub('%[%[[^%]|]*|','') :gsub('[%[%]]','') :gsub('\226\128\140','') return mw.ustring.len(s) end local function td(class, colspan, style, content) local s = '<td' if class and class ~= '' then s = s .. ' class="' .. class .. '"' end if colspan and colspan > 1 then s = s .. ' colspan="' .. colspan .. '"' end if style and style ~= '' then s = s .. ' style="' .. esc(style) .. '"' end return s .. '>' .. (content or '') .. '</td>' end -- مصراع: فقط wrap در span؛ ارجاعها inline میمانند. local function hemiCell(text) return '<span class="beyt">' .. (text or '') .. '</span>' end local function rowOpen(d, kind, noId) local cls = 'poem-row ' .. kind if d.class then cls = cls .. ' ' .. d.class end local id = '' if d.id and not noId then id = ' id="' .. esc(d.id) .. '"' end return '<tr class="' .. esc(cls) .. '"' .. id .. '>' end local function rowBayt(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.bayt return rowOpen(d, 'poem-bayt') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, joinStyle(alignFor(j, 1), d.style), hemiCell(m1)) .. td('poem-num poem-num-c', 1, d.style, d.c) .. td('b poem-m poem-m2', 2, joinStyle(alignFor(j, 2), d.style), hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowStagger(d) local m1 = d.m1 if m1 == '' and d.m2 == '' then m1 = cfg.nbsp end local j = d.just or cfg.justDefault.stagger local s1 = joinStyle(alignFor(j, 1), d.style) local s2 = joinStyle(alignFor(j, 2), d.style) local ind = d.indent if ind and ind ~= '' and ind ~= '0' and ind ~= '0%' and ind ~= '0em' then s1 = joinStyle('padding-inline-end:' .. ind, s1) s2 = joinStyle('padding-inline-start:' .. ind, s2) end return rowOpen(d, 'poem-stagger poem-stagger-1') .. td('poem-num poem-num-r', 1, 'text-align:left', d.r) .. td('b poem-m poem-m1', 2, s1, hemiCell(m1)) .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-l poem-empty', 1, nil, '') .. '</tr>\n' .. rowOpen(d, 'poem-stagger poem-stagger-2', true) .. td('poem-num poem-num-r poem-empty', 1, nil, '') .. td('poem-empty', 2, nil, '') .. td('poem-num poem-num-c poem-empty', 1, nil, '') .. td('b poem-m poem-m2', 2, s2, hemiCell(d.m2)) .. td('poem-num poem-num-l', 1, 'text-align:right', d.l) .. '</tr>' end local function rowMesra(d) local align = d.align or 'center' local content = d.text if d.blank and content == '' then content = cfg.nbsp end local inline = (align ~= 'center') and ALIGN_STYLE[align] or nil local style = joinStyle(inline, d.style) local cellClass = 'b poem-m' local cs = { 2, 3, 2 } if align ~= 'center' then cs = { 1, 5, 1 } cellClass = cellClass .. ' poem-m-' .. align if d.indent then style = 'padding-' .. align .. ':' .. d.indent .. ';' .. style end end local cell if d.blank and content == cfg.nbsp then cell = content else cell = hemiCell(content) end return rowOpen(d, 'poem-mesra') .. td('poem-num poem-num-r', cs[1], 'text-align:left', d.r) .. td(cellClass, cs[2], style, cell) .. td('poem-num poem-num-l', cs[3], 'text-align:right', d.l) .. '</tr>' end local function rowGap(d) local style = d.height and ('height:' .. d.height) or nil local content = d.height and '' or cfg.nbsp return '<tr class="poem-row poem-gap' .. (d.class and (' ' .. esc(d.class)) or '') .. '">' .. td('poem-gapcell', 7, style, content) .. '</tr>' end local RENDER = { bayt = rowBayt, stagger = rowStagger, mesra = rowMesra } local function sizerRow() return '<tr class="poem-sizer" aria-hidden="true">' .. '<td class="poem-s"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-g"></td>' .. '<td class="poem-mh"></td><td class="poem-mh"></td>' .. '<td class="poem-s"></td>' .. '</tr>' end local function openHtml(o) local cls = 'poem' .. (o.fixed and ' poem-fixed' or '') if o.class then cls = cls .. ' ' .. o.class end local div = '<div class="' .. esc(cls) .. '"' if o.lang then div = div .. ' lang="' .. o.lang .. '"' end if o.dir then div = div .. ' dir="' .. o.dir .. '"' end local font = o.font and ('font-family:' .. o.font) or nil local tcls = 'poem-table' .. (o.nastaliq and ' nastaliq' or '') if o.fixed then local wrap = joinStyle('width:' .. o.width, 'max-width:' .. o.maxWidth, 'margin:0 auto', font, o.style) return div .. ' style="' .. esc(wrap) .. '"><table class="' .. tcls .. '" role="presentation" style="width:100%;table-layout:fixed">' .. sizerRow() end local style = 'margin: 0 auto;' if font then style = style .. font .. ';' end if o.style then style = style .. ' ' .. o.style end return div .. '><table class="' .. tcls .. '" role="presentation" style="' .. esc(style) .. '">' end --------------------------------------------------------------------------- -- چندصفحهای --------------------------------------------------------------------------- local function joinText(a, b, sep) if a == '' then return b end if b == '' then return a end return a .. sep .. b end local function mergeRow(head, tail, sep) for _, f in ipairs({ 'm1', 'm2', 'text' }) do if head[f] ~= nil or tail[f] ~= nil then head[f] = joinText(head[f] or '', tail[f] or '', sep) end end for _, f in ipairs({ 'r', 'c', 'l' }) do if tail[f] and tail[f] ~= '' then head[f] = tail[f] end end head.style = head.style or tail.style head.class = head.class or tail.class head.id = head.id or tail.id return head end local function flushHtml() local c = state.carry if not c then return '' end state.carry = nil return RENDER[c.kind](c.d) end local function plain(html) if not inPageNs() then local pre = flushHtml() if pre ~= '' then return pre .. '\n' .. html end end return html end local function emit(kind, d, opt, flags) opt = opt or {} if inPageNs() then if opt.unfinished then d.class = addClass(d.class, 'poem-partial-end') end if opt.continued then d.class = addClass(d.class, 'poem-partial-start') end return RENDER[kind](d) end if not cfg.mergeRows then return RENDER[kind](d) end local out = '' local carry = state.carry state.carry = nil if carry then if opt.continued and carry.kind == kind then d = mergeRow(carry.d, d, opt.joiner or ' ') else out = RENDER[carry.kind](carry.d) if opt.continued then flags.unknown = true end end end if opt.unfinished then state.carry = { kind = kind, d = d } return out ~= '' and out or SKIP end local html = RENDER[kind](d) if out ~= '' then return out .. '\n' .. html end return html end local function rowOpts(args, flags) local opt = {} local u = pick(args, N.unfinished) if u then local b = bool(u, nil); if b == nil then flags.unknown = true else opt.unfinished = b end end local c = pick(args, N.continued) if c then local w = WHICH[c:lower()] if w then opt.continued, opt.which = true, w else local b = bool(c, nil); if b == nil then flags.unknown = true else opt.continued = b end end end local j = pick(args, N.joiner) if j then opt.joiner = JOIN[j:lower()]; if opt.joiner == nil then flags.unknown = true end end return opt end local function parseOpen(args, flags) local o = {} local isText = present(args, N.text) for i = 1, 2 do local v = args[i] if v and v ~= '' then local lv = v:lower() if v == 'نستعلیق' or lv == 'nastaliq' then o.nastaliq = true elseif v == 'ادامه' or lv == 'continue' then if isText then flags.unknown = true else o.cont = 'prev' end else flags.unknown = true end end end if not o.nastaliq then o.nastaliq = bool(pick(args, N.nastaliq), false) end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k then if not isText and k == 'next' then flags.unknown = true; k = nil end else local b = bool(c, nil) if b == nil then flags.unknown = true elseif b then if isText then flags.unknown = true else k = 'prev' end end end if k then o.cont = k end end o.style = pick(args, N.style); o.class = pick(args, N.class) local lang = pick(args, N.lang) if lang then if lang:match('^%a[%w%-]*$') then o.lang = lang else flags.unknown = true end end local dir = pick(args, N.dir) if dir then dir = dir:lower() if dir == 'rtl' or dir == 'ltr' then o.dir = dir else flags.unknown = true end end if o.lang and not o.dir then o.dir = langDir(o.lang) end o.kind = 'bayt' local k = pick(args, N.kind) if k then o.kind = KIND[k:lower()]; if not o.kind then o.kind = 'bayt'; flags.unknown = true end end local a = pick(args, N.align) if a then o.align = ALIGN[a:lower()]; if not o.align then flags.unknown = true end end local g = pick(args, N.gap) if g then o.gap = parseLength(g); if not o.gap then flags.unknown = true end end local ind = pick(args, N.indent) if ind then o.indent = parseLength(ind); if not o.indent then flags.unknown = true end end o.unitNum, o.unitUnit = splitLength(pick(args, N.unit)) if not o.unitNum then o.unitNum, o.unitUnit = splitLength('1.5em'); if pick(args, N.unit) then flags.unknown = true end end local st = pick(args, N.stanza) if st then if STANZA[st:lower()] then o.newStanza = true else flags.unknown = true end end local jv = pick(args, N.just) if jv then o.just = JUST[jv:lower()]; if not o.just then flags.unknown = true end end if o.nastaliq then o.font = cfg.nastaliqFonts end local f = pick(args, N.font) if f then if f == 'نستعلیق' then o.nastaliq, o.font = true, cfg.nastaliqFonts elseif f:match("^[%w%s,'%-%._\128-\255]+$") then o.nastaliq, o.font = false, f else flags.unknown = true end end local w = pick(args, N.width) if w then local lw = w:lower() if lw == 'خودکار' or lw == 'auto' then o.fixed = false else o.width = parseLength(w); if o.width then o.fixed = true else flags.unknown = true end end end local mx = pick(args, N.maxwidth) if mx then local lm = mx:lower() if lm == 'none' or lm == 'بدون' or lm == 'نامحدود' then o.maxWidth = 'none' else o.maxWidth = parseLength(mx); if not o.maxWidth then flags.unknown = true end end end if o.fixed == nil then o.fixed = (cfg.defaultWidth ~= 'auto') end if o.fixed then local explicit = (o.width ~= nil) o.width = o.width or cfg.defaultWidth o.maxWidth = o.maxWidth or (explicit and '100%' or cfg.maxWidth) end return o end local function parseClose(args, flags) local cont = false local v = args[1] if v and v ~= '' then if v == 'ادامه' or v:lower() == 'continue' then cont = true else flags.unknown = true end end local c = pick(args, N.cont) if c then local k = CONT[c:lower()] if k == 'next' or k == 'both' then cont = true elseif k == 'prev' then flags.unknown = true else local b = bool(c, nil); if b == nil then flags.unknown = true elseif b then cont = true end end end return cont end local function parseLine(raw, freeVerse) local t = {} local s = raw if freeVerse then t.level = #s:match('^:*'); s = s:sub(t.level + 1) end if s:sub(1, 2) == '<>' then t.align = 'center'; s = s:sub(3) elseif s:sub(1, 2) == '>>' and s:sub(3, 3) ~= '>' then t.align = 'right'; s = s:sub(3) end s = mw.text.trim(s) local pre, rest = s:match('^(.-)%s*<<<%s*(.*)$') if pre then t.pre = pre; s = rest end local body, post = s:match('^(.-)%s*>>>%s*(.+)$') if body then t.post = post; s = body end t.text = s return t end local function parseDirective(line) local rest = mw.text.trim(line:sub(2)) local lrest = rest:lower() for _, key in ipairs(KIND_KEYS) do if lrest:sub(1, #key) == key then local after = rest:sub(#key + 1) if after == '' or after:match('^%s') then local dd, bad = { kind = KIND[key] }, false for k, v in after:gmatch('(%S+)=(%S+)') do local kk, lv = k:lower(), v:lower() if kk == 'تورفتگی' or kk == 'indent' then dd.indent = parseLength(v); if not dd.indent then bad = true end elseif kk == 'تراز' or kk == 'align' then dd.align = ALIGN[lv]; if not dd.align then bad = true end elseif kk == 'ترازبندی' or kk == 'justify' then dd.just = JUST[lv]; if not dd.just then bad = true end else bad = true end end return dd, bad end end end return nil, true end local function renderLines(text, o, ctx, ropt, flags) local cur = { kind = o.kind, indent = o.indent, align = o.align, just = o.just } local items, pending, pendingCur, needGap = {}, nil, nil, false local function info(c) local twoH = (c.kind ~= 'mesra') local rowKind = (c.kind == 'mesra') and 'mesra' or ((c.kind == 'bayt') and 'bayt' or 'stagger') return twoH, rowKind, c.indent end local function numbers(d, t) if not t then return end if t.pre and t.pre ~= '' and d.r == '' then d.r = convertDigits(t.pre, ctx.digits) end if t.post and t.post ~= '' and d.l == '' then d.l = convertDigits(t.post, ctx.digits) end end local function makeBayt(t1, t2, c) local _, _, indent = info(c) local d = { m1 = t1 and clean(t1.text) or '', m2 = t2 and clean(t2.text) or '', r = '', c = '', l = '', indent = indent, fixed = ctx.fixed, just = c.just } numbers(d, t1); numbers(d, t2) return d end local function makeMesra(t, c, free) local d = { text = clean(t.text), r = '', c = '', l = '', fixed = ctx.fixed } d.align = t.align or c.align or (free and 'right' or 'center') if free and (t.level or 0) > 0 and d.align ~= 'center' then d.indent = string.format('%g', o.unitNum * t.level) .. o.unitUnit end numbers(d, t) return d end local function push(kind, d, counted) items[#items + 1] = { kind = kind, d = d, counted = counted, gap = needGap and #items > 0 } needGap = false end local function flushPending() if pending then push('mesra', makeMesra(pending, pendingCur, false), false); pending = nil end end local src = text:gsub('\r\n?', '\n') for raw in (src .. '\n'):gmatch('([^\n]*)\n') do local line = mw.text.trim(raw) if line == '' then flushPending() if #items > 0 then needGap = true end elseif line:sub(1, 1) == '@' then local dd, bad = parseDirective(line) flushPending() if dd then cur = { kind = dd.kind, indent = dd.indent or o.indent, align = dd.align or o.align, just = dd.just or o.just } end if bad then flags.unknown = true end else local twoH, rowKind = info(cur) local isFirst = (#items == 0 and not pending) local t = parseLine(line, not twoH) if not twoH then push('mesra', makeMesra(t, cur, true), ctx.mesra) else local handled = false if ropt.continued and isFirst then local mode = ropt.which or 'auto' if mode == 'auto' then local c = state.carry mode = (c and c.kind ~= 'mesra' and (c.d.m2 or '') == '') and 'second' or 'both' end if mode == 'first' then push(rowKind, makeBayt(t, nil, cur), true); handled = true elseif mode == 'second' then push(rowKind, makeBayt(nil, t, cur), true); handled = true end end if not handled then if pending then push(rowKind, makeBayt(pending, t, pendingCur), true); pending = nil else pending, pendingCur = t, cur end end end end end if pending then local _, rowKind = info(pendingCur) if ropt.unfinished then push(rowKind, makeBayt(pending, nil, pendingCur), true) else push('mesra', makeMesra(pending, pendingCur, false), false) end pending = nil end for i, it in ipairs(items) do if it.counted and not (i == 1 and ropt.continued) then applyAuto(it.d, ctx, true, it.kind ~= 'bayt') end end local out = {} for i, it in ipairs(items) do if it.gap then out[#out + 1] = plain(rowGap({ height = o.gap })) end local ro = {} if i == 1 and ropt.continued then ro.continued, ro.joiner = true, ropt.joiner end if i == #items and ropt.unfinished then ro.unfinished = true end local h = emit(it.kind, it.d, ro, flags) if h ~= SKIP and h ~= '' then out[#out + 1] = h end end return table.concat(out, '\n') end local function open(frame) local args, flags = getArgs(frame), {} local o = parseOpen(args, flags) local isText = present(args, N.text) local page = inPageNs() if hasUnknown(args, 2) then flags.unknown = true end local joins = (not page) and state.open and (o.cont == 'prev' or o.cont == 'both') local ctx if joins and state.ctx then ctx = state.ctx else ctx = newCtx(args, o.kind, flags) ctx.fixed, ctx.just = o.fixed, o.just end local parts = {} if not joins then parts[#parts + 1] = stylesTag(frame) .. openHtml(o) state.carry = nil end state.open, state.ctx = true, ctx if isText then local ropt = rowOpts(args, flags) if o.newStanza and joins and not ropt.continued then parts[#parts + 1] = plain(rowGap({ height = o.gap })) end local rows = renderLines(pick(args, N.text) or '', o, ctx, ropt, flags) if rows ~= '' then parts[#parts + 1] = rows end local stay = (not page) and (o.cont == 'next' or o.cont == 'both') if not stay then local tail = flushHtml() if tail ~= '' then parts[#parts + 1] = tail end parts[#parts + 1] = CLOSE_HTML state.open, state.ctx = false, nil else parts[#parts + 1] = FINAL_MARKER end end if #parts == 0 then parts[1] = SKIP end return table.concat(parts, '\n') .. categories(flags) end local function close(frame) local args, flags = getArgs(frame), {} local cont = parseClose(args, flags) if hasUnknown(args, 1) then flags.unknown = true end local html = flushHtml() if html ~= '' then html = html .. '\n' end if cont and not inPageNs() then return html .. FINAL_MARKER .. categories(flags) end state.open, state.ctx = false, nil return html .. CLOSE_HTML .. categories(flags) end local function commonRow(args, ctx, flags) local d = { r = numberArg(args, N.right, ctx.digits), c = numberArg(args, N.center, ctx.digits), l = numberArg(args, N.left, ctx.digits), style = pick(args, N.style), class = pick(args, N.class), id = pick(args, N.id), fixed = ctx.fixed, just = ctx.just, } local jv = pick(args, N.just) if jv then d.just = JUST[jv:lower()] if not d.just then flags.unknown = true; d.just = ctx.just end end return d end local function bayt(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, false) local html = emit('bayt', d, opt, flags) return html .. categories(flags) end local function staggerFn(defaultIndent) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.m1, d.m2 = clean(args[1]), clean(args[2]) if (d.m1 == '') ~= (d.m2 == '') and not opt.unfinished and not opt.continued then flags.incomplete = true end local raw = pick(args, N.indent) if raw then d.indent = parseLength(raw) if not d.indent then flags.unknown = true end d.indent = d.indent or defaultIndent else d.indent = defaultIndent end if hasUnknown(args, 2) then flags.unknown = true end applyAuto(d, ctx, not opt.continued, true) local html = emit('stagger', d, opt, flags) return html .. categories(flags) end end local function mesra(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local opt = rowOpts(args, flags) local d = commonRow(args, ctx, flags) d.text = clean(args[1]) local a = pick(args, N.align) if a then d.align = ALIGN[a:lower()]; if not d.align then flags.unknown = true end end local raw = pick(args, N.indent) d.indent = parseLength(raw) if raw and not d.indent then flags.unknown = true end if hasUnknown(args, 1) then flags.unknown = true end applyAuto(d, ctx, ctx.mesra and not opt.continued, true) local html = emit('mesra', d, opt, flags) return html .. categories(flags) end local function emptyFn(kind) return function(frame) local args = getArgs(frame) local ctx = state.ctx or defaultCtx() local flags = {} local d = commonRow(args, ctx, flags) local raw = pick(args, N.height) d.height = parseLength(raw) if raw and not d.height then flags.unknown = true end if hasUnknown(args, 0) then flags.unknown = true end local html if d.height then html = rowGap(d) elseif kind == 'bayt' then d.m1, d.m2 = '', ''; html = rowBayt(d) else d.text, d.blank = '', true; html = rowMesra(d) end return plain(html) .. categories(flags) end end local function safe(fn) return function(frame) local ok, res = pcall(fn, frame) if ok then return res end return '<span class="error">خطا در پودمان شعر: ' .. esc(tostring(res)) .. '</span>' .. categories({ error = true }) end end p.open = safe(open) p.close = safe(close) p.bayt = safe(bayt) p.stagger = safe(staggerFn(nil)) p.ladder = safe(staggerFn(nil)) p.mesra = safe(mesra) p.emptyBayt = safe(emptyFn('bayt')) p.emptyMesra = safe(emptyFn('mesra')) p.config = cfg return p 3zjws7bplum20owc51ycqcxzk6oewrv برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۸ 104 97060 304015 302757 2026-10-11T20:31:31Z Hanooz 17889 304015 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر|۴|بدایع شیخ سعدی|}}</noinclude>{{وسط|{{اندازه قلم|110%|'''۳'''}}}} {{وسط|<nowiki>[---⏑|---⏑|---⏑|---⏑]</nowiki> هزج مثمّن سالم :Metre}} {{شعر۲|نوع=ضربدری|متن= ز حد بگذشت مشتاقی و صبر اندر غمت مارا!<ref>R; یارا, for مارا.</ref> بوصل خود دوائی کن دل دیوانهٔ ما را علاج درد مشتاقان طبیب عام نشناسد مگر لیلی کند درمان غم مجنون شیدا را گرت پروای غمگینان نخواهد بود و مسکینان نبایستی نمود اوّل بما آن روی زیبارا چو بنمودی و بربودی ثبات از عقل و صبر از دل بباید چارهٔ کردن کنون این ناشکیبارا مرا سودای بت رویان نبودی پیش ازین در سر و لیکن تاترا دیدم گزیدم راه سودا را مراد ما وصال تست از دنیی و از عقبی و گرنه بی شما قدری نباشد دین و دنیارا چنان مشتاقم ای دلبر بدیدارت که گر روزی بر آید از دلم آهی بسوزد هفت دریارا بیا تا یک زمان امروز خوش باشیم در خلوت که در عالم نمیداند کسی احوال فردا را سخن شیرین همی گوئی برغم دشمنان سعدی ولی بیمار استسقا چه داند ذوق حلوارا }} {{چپ|N. B. This ode is only found in I and R.}}<noinclude>{{خطکش}} {{پانویس|چپ‌چین=بله}}</noinclude> 1kubmxdh1wso2w3vxpf8ihqggiegzf9 برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۹ 104 97061 303991 302768 2026-10-11T18:09:50Z Hanooz 17889 303991 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||بدایع شیخ سعدی|۵}}</noinclude><section begin="p19-20" /> {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}} {{شعر|نوع=ضربدری|ادامه=بعد|متن= با جوانی سرخوش است این پیر بی تدبیر را جهل باشد با جوانان پنجه کردن پیر را من که با موئی بقوّت بر نیایم ایعجب با یکی<ref>I(a); کسی, for یکی.</ref> افتاده ام کو بگسلد زنجیر را چون کمان در بازو آرد سروقدّ سیمتن آرزویم میکند<ref>L, Z; کشد, for کند.</ref> کآماج باشم تیر<ref>This line is omitted in R.</ref> را میرود تا در کمند افتد بپای خویشتن گر بر آن دست<ref>L, Z; تیر, for دست.</ref> و کمان چشم اوفتد نخجیر را<ref>In R the second hemistich runs as follows:— <div dir="rtl">«چون تو چوگان میزنی جرمی نباشد تیر را».</div></ref> کس ندید است آدمیزاد<ref>R; را, for زاد.</ref> از تو شیرین تر سخن شکر از پستان مادر خوردهٔ یا شیر را روز بازار جوانی پنج<ref>C, Y(a), Y(b); چند, for پنج.</ref> روزی بیش نیست نقد را باش ای پسر کآفت بود تأخیر را ایکه گفتی دیده از دیدار بت رویان<ref>I(a), R, L, C, Y(a), Y(b); مه, for بت.</ref> بدوز هر چه گوئی چاره دانم کرد جز تقدیر را }}<noinclude>{{خطکش}} {{پانویس|چپ‌چین=بله}}</noinclude> sshnuf5hn2z3nidm60o5t4u7v6jw4tz 303992 303991 2026-10-11T18:10:24Z Hanooz 17889 303992 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||بدایع شیخ سعدی|۵}}</noinclude><section begin="p19-20" /> {{وسط|{{اندازه قلم|110%|'''۴'''}}}} {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}} {{شعر|نوع=ضربدری|ادامه=بعد|متن= با جوانی سرخوش است این پیر بی تدبیر را جهل باشد با جوانان پنجه کردن پیر را من که با موئی بقوّت بر نیایم ایعجب با یکی<ref>I(a); کسی, for یکی.</ref> افتاده ام کو بگسلد زنجیر را چون کمان در بازو آرد سروقدّ سیمتن آرزویم میکند<ref>L, Z; کشد, for کند.</ref> کآماج باشم تیر<ref>This line is omitted in R.</ref> را میرود تا در کمند افتد بپای خویشتن گر بر آن دست<ref>L, Z; تیر, for دست.</ref> و کمان چشم اوفتد نخجیر را<ref>In R the second hemistich runs as follows:— <div dir="rtl">«چون تو چوگان میزنی جرمی نباشد تیر را».</div></ref> کس ندید است آدمیزاد<ref>R; را, for زاد.</ref> از تو شیرین تر سخن شکر از پستان مادر خوردهٔ یا شیر را روز بازار جوانی پنج<ref>C, Y(a), Y(b); چند, for پنج.</ref> روزی بیش نیست نقد را باش ای پسر کآفت بود تأخیر را ایکه گفتی دیده از دیدار بت رویان<ref>I(a), R, L, C, Y(a), Y(b); مه, for بت.</ref> بدوز هر چه گوئی چاره دانم کرد جز تقدیر را }}<noinclude>{{خطکش}} {{پانویس|چپ‌چین=بله}}</noinclude> 50ez15m3iyzsnky5pfb07770227ukph 304012 303992 2026-10-11T20:30:20Z Hanooz 17889 304012 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||بدایع شیخ سعدی|۵}}</noinclude><section begin="p19-20" /> {{وسط|{{اندازه قلم|110%|'''۴'''}}}} {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلن}} {{شعر۲|نوع=ضربدری|ادامه=بعد|متن= با جوانی سرخوش است این پیر بی تدبیر را جهل باشد با جوانان پنجه کردن پیر را من که با موئی بقوّت بر نیایم ایعجب با یکی<ref>I(a); کسی, for یکی.</ref> افتاده ام کو بگسلد زنجیر را چون کمان در بازو آرد سروقدّ سیمتن آرزویم میکند<ref>L, Z; کشد, for کند.</ref> کآماج باشم تیر<ref>This line is omitted in R.</ref> را میرود تا در کمند افتد بپای خویشتن گر بر آن دست<ref>L, Z; تیر, for دست.</ref> و کمان چشم اوفتد نخجیر را<ref>In R the second hemistich runs as follows:— <div dir="rtl">«چون تو چوگان میزنی جرمی نباشد تیر را».</div></ref> کس ندید است آدمیزاد<ref>R; را, for زاد.</ref> از تو شیرین تر سخن شکر از پستان مادر خوردهٔ یا شیر را روز بازار جوانی پنج<ref>C, Y(a), Y(b); چند, for پنج.</ref> روزی بیش نیست نقد را باش ای پسر کآفت بود تأخیر را ایکه گفتی دیده از دیدار بت رویان<ref>I(a), R, L, C, Y(a), Y(b); مه, for بت.</ref> بدوز هر چه گوئی چاره دانم کرد جز تقدیر را }}<noinclude>{{خطکش}} {{پانویس|چپ‌چین=بله}}</noinclude> 6d0tvt4f1u7t27mgi88ra3bbhmm9bm0 برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۲۰ 104 97062 304016 302767 2026-10-11T20:31:47Z Hanooz 17889 304016 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر|۶|بدایع شیخ سعدی|}}</noinclude>{{شعر۲|نوع=ضربدری|ادامه=قبل|متن= زهد پیدا کفر پنهان بود چندین روزگار پرده از سر بر گرفتم<ref>L; گرفتیم, for گرفتم.</ref> اینهمه تزویررا<ref>I, P, R; آن, for این.</ref> سعدیا در پای جانان گر بخدمت سر نهی همچنان عذرت بباید خواستن تقصیر را }} <section end="p19-20" /> <section begin="p20-21" /> {{وسط|{{اندازه قلم|110%|'''۵'''}}}} {{وسط|<nowiki>[--⏑|⏑--⏑|⏑--⏑|⏑--]</nowiki> هزج مثمّن اخرب مکفوف محذوف :Metre}} {{شعر۲|ادامه=بعد|متن= ساقی بده آن کوزهٔ یاقوت روانرا یاقوت چه باشد<ref>In R the hemistich 8(b) is substituted, for 7(b), and 7(b) & 8(a) are omitted.</ref> بده آن قوت روانرا اول پدر پیر خورد رطل دمادم تا مدّعیان هیچ نگویند<ref>I(a), P, R, C; ارزد, for باشد.</ref> جوانرا تا مست نباشی نکشی بار غم یار آری شتر مست کشد<ref>L, Y(a), Y(b); خورده نگیرند, for هیچ نگویند: I(a); عیب نگیرند, for do.</ref> بار گرانرا ای روی تو آرام دل خلق جهانی بیروی تو شاید که نه بینند جهانرا در صورت و معنی که تو داری چه توانگفت حسن تو ز تحسین تو بسته است زبانرا اینک عسلی دوخته دارد<ref>I; عسل اندوخت آنک, for اینک عسلی دوخته.</ref> مگس نحل شهد لب شیرین تو زنبور میانرا زین دست که دیدار تو دل میبرد از دست ترسم نبرم عاقبت از دست تو جانرا یا<ref>L, Y(b); تا, for یا.</ref> تیر هلاکم بزنی بر دل مجروح تا<ref>R, C, Y(a), Y(b); یا for تا.</ref> جان بدهم یا بدهی تیر امانرا وانگه که بتیرم زنی اوّل خبرم کن تا پیشترت بوسه دهم دست و کمانرا }}<noinclude>{{خطکش}} {{پانویس|چپ‌چین=بله}}</noinclude> 6ml97p02vcqh0teyf4uzq031laclnf3 برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۹ 104 97124 303959 303939 2026-10-11T14:33:07Z Hanooz 17889 303959 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" /></noinclude>{{وسط|'''مقدّمه'''}} بدایع شیخ مصلح الدین سعدی شیرازی که خود از بدایع روزگار بود یکی از نفیسترین قسمتهای گنجینهٔ کلیات این شاعر بزرگوار را تشکیل میدهد. در خصوص مؤلّفات و شرح حیات سعدی تحقیقات زیادی بعمل امده علمای چند عمری مصروف این خدمت نموده‌اند. بهمین ملاحظه ما در اینجا از ذکر جزئیّات احوال شاعر صرف نظر نموده فقط بایراد بعضی ملاحظات میپردازیم؛ کتاب موسوم به {{w|تحقیق درباره سعدی|ESSAI SUR LE POET SAʿDI}} (تحقیقات دربارهٔ سعدی شاعر) که در سال ۱۹۱۹ مسیحی بتوسط یکنفر فرانسوی موسوم به «{{w|هانری ماسه}}» در پاریس انتشار یافته است و شاید بهترین رساله‌ای باشد که در این موضوع تألیف شده کتاب بسیار نفیس و جامعی است، این کتاب در ۳۴۷ صفحه شامل چهار بابست و هر باب چند فصل دارد که ما برای استحضار خوانندگان مختصراً بدانها اشاره میکنیم: باب اول در خصوص شخص سعدی، باب دوّم در تحقیق عقاید و افکار او، باب سوّم در صفت شاعری او، باب چهارم در خصوص کتب و رسالهائیست که تا کنون دربارهٔ سعدی بالسنهٔ مختلفه تألیف شده، از اینرو این کتاب جامع تمام تحقیقات راجع بسعدی است. علاوه بر این در کتاب تاریخ ادبیات ایران زبان انگلیسی A LITERARY HISTORY OF PERSIA. (جلد از فردوسی تا سعدی صفحه ۳۲۶) تألیف {{w|ادوارد براون|براون}} و در کتاب «اساس فیلولوژی ایرانی» بزبان آلمانی [[:de:Grundriss der iranischen Philologie|GRUNDRISS DER IRANISCHEN PHILOLOGIE]]. بقلم {{w|هرمان اته|اِته}} شرح مفیدی راجع بسعدی موجود است.<ref>کتاب اخیر شاید بهترین و جامعترین کتابیست که دربارهٔ زبان فارسی نوشته شده.</ref> سعدی بعقیدهٔ مشهور در شیراز بسال ۵۸۰ متولد و ۶۹۰ یا ۶۹۱ وفات یافته است و معاصر با {{w|اتابکان فارس}} بوده نام پدرش عبدالله ولی اسم خود<noinclude>{{خطکش}} {{پانویس-کوچک}}</noinclude> 7tb9is6cxflt9ub756je704zv162xqk برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۰ 104 97129 303960 303949 2026-10-11T14:43:22Z Hanooz 17889 /* نمونه‌خوانی‌شده */ 303960 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" /> {{سرصفحه‌مستمر||(۴)|}}</noinclude>شاعر معلوم نیست و شیخ مصلح الدین که بسعدی خطاب میشود نام پدر اوست و لقب خودش شیخ مشرف الدین است و سعدی تخلص شاعرانهٔ اوست که بمناسبت اسم ممدوح خود اختیار کرده. در آخر نسخه‌ای که در این کتاب زیر صفحات جزء یادداشتها با علامت I. بدان اشاره میشود (شرح این نسخه در زیر خواهد آمد) و تاریخ تحریر ان سی و شش سال پس از وفات شاعر است عبارت عربی ذیل موجود است (نقلت من خطّ المصنّف و هو مشرف الدین بن مصلح الدین عبدالله) این عبارت دو اهمّیت دارد یکی اینکه از روی ان بخوبی واضح میشود که نام سعدی مشرف الدین است و مصلح الدین اسم پدر اوست دیگر اینکه صحّت و قدمت نسخه سابق الذکر را ثابت نموده آشکار میکند که این نسخه از روی خط خود سعدی استنساخ شده است و بهمین نظر این نسخه اهمّیت زیاد در نظر محققین دارد ولی متأسفانه ناقص است. سعدی از دوازده سالگی شروع بمسافرت کرده سی سال از زندگانی صد و اند سالهٔ خود را در {{w|نظامیه بغداد|مدرسهٔ نظامیّهٔ بغداد}} بتحصیل علوم پرداخته سی سال دیگر همواره در سفر بوده، مسافرتهای او را اگر چه نمیتوان با ترتیب و تاریخ دقیق معین کرد ولی آنچه که از اشارات مختلفهٔ {{w|گلستان سعدی|گلستان}} و {{w|بوستان سعدی|بوستان}} او بر میآید به بین النهرین و آسیای صغیر و مصر و مراکش و شام و بیلقان (شاید از شهر تبریز) و کجرات و غیره سفر کرده و مسافرت او در مغرب مصادف با جنگهای صلیبی بوده و سعدی اسیر شده. پس از سی سال جهانگردی بشیراز آمده و در گوشه‌ای بیرون شیراز رحل اقامت افگنده شاید اغلب آثار خود را نیز در همان زمان تألیف نموده است و اگر تاریخ تولد که ذکر کردیم صحیح باشد سعدی گلستان خود را مطابق شعری که میگوید: {{شعر|متن= در آن مدت که ما را وقت خوش بود ز هجرت ششصد و پنجاه و شش بود }} در سنّ هفتاد و شش سالگی پس از مسافرتها و جهانگردیها تألیف نموده. این شاعر بزرگوار در زمان حیات خود نیز مورد توجّه بوده، تربتش بیرون شهر شیراز در باغ باصفا و دلگشائیست.<noinclude></noinclude> p36p9qu2uybfyzf1prha11vbl2pk208 304013 303960 2026-10-11T20:30:58Z Hanooz 17889 304013 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" /> {{سرصفحه‌مستمر||(۴)|}}</noinclude>شاعر معلوم نیست و شیخ مصلح الدین که بسعدی خطاب میشود نام پدر اوست و لقب خودش شیخ مشرف الدین است و سعدی تخلص شاعرانهٔ اوست که بمناسبت اسم ممدوح خود اختیار کرده. در آخر نسخه‌ای که در این کتاب زیر صفحات جزء یادداشتها با علامت I. بدان اشاره میشود (شرح این نسخه در زیر خواهد آمد) و تاریخ تحریر ان سی و شش سال پس از وفات شاعر است عبارت عربی ذیل موجود است (نقلت من خطّ المصنّف و هو مشرف الدین بن مصلح الدین عبدالله) این عبارت دو اهمّیت دارد یکی اینکه از روی ان بخوبی واضح میشود که نام سعدی مشرف الدین است و مصلح الدین اسم پدر اوست دیگر اینکه صحّت و قدمت نسخه سابق الذکر را ثابت نموده آشکار میکند که این نسخه از روی خط خود سعدی استنساخ شده است و بهمین نظر این نسخه اهمّیت زیاد در نظر محققین دارد ولی متأسفانه ناقص است. سعدی از دوازده سالگی شروع بمسافرت کرده سی سال از زندگانی صد و اند سالهٔ خود را در {{w|نظامیه بغداد|مدرسهٔ نظامیّهٔ بغداد}} بتحصیل علوم پرداخته سی سال دیگر همواره در سفر بوده، مسافرتهای او را اگر چه نمیتوان با ترتیب و تاریخ دقیق معین کرد ولی آنچه که از اشارات مختلفهٔ {{w|گلستان سعدی|گلستان}} و {{w|بوستان سعدی|بوستان}} او بر میآید به بین النهرین و آسیای صغیر و مصر و مراکش و شام و بیلقان (شاید از شهر تبریز) و کجرات و غیره سفر کرده و مسافرت او در مغرب مصادف با جنگهای صلیبی بوده و سعدی اسیر شده. پس از سی سال جهانگردی بشیراز آمده و در گوشه‌ای بیرون شیراز رحل اقامت افگنده شاید اغلب آثار خود را نیز در همان زمان تألیف نموده است و اگر تاریخ تولد که ذکر کردیم صحیح باشد سعدی گلستان خود را مطابق شعری که میگوید: {{شعر۲|متن= در آن مدت که ما را وقت خوش بود ز هجرت ششصد و پنجاه و شش بود }} در سنّ هفتاد و شش سالگی پس از مسافرتها و جهانگردیها تألیف نموده. این شاعر بزرگوار در زمان حیات خود نیز مورد توجّه بوده، تربتش بیرون شهر شیراز در باغ باصفا و دلگشائیست.<noinclude></noinclude> cjvqx5vbkp76ypflh5x5828lef5ek1g برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۱ 104 97130 303961 303950 2026-10-11T15:14:05Z Hanooz 17889 /* نمونه‌خوانی‌شده */ 303961 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||(۵)|}}</noinclude>تألیفات سعدی چون نتیجه و حاصل تحصیلات و تجربیات طولانیست بسیار پر معنی و در نهایت درجه اهمیت است و از لطیفترین محصولات گلستان ادبیّات فارسی میباشد. مجموع تألیفات شاعر را تقریباً سی و پنج سال پس از وفات او شخصی موسوم به {{w|ابوبکر بیستون|علی بن احمد بن ابی بکر بیستون}} جمع کرده، نخستین بار کلیّات سعدی (شامل رسالات، گلستان، بوستان، پندنامه، قصائد عربیه، قصائد فارسی، قطعات، ملمّعات، طیّبات، بدایع، خواتیم، غزلیّات قدیمه، خبیثات، رباعیّات و مفردات) بدستیاری مولوی محمد رشید و هارینگتون ({{w|:en:John Herbert Harington|Harington}}) انگلیسی در سال ۱۲۰۹ در کلکته بخط نستعلیق چاپ شده که بهترین چاپهای کلیات است. پس از آن تا کنون قریب بیست بار دیگر کلیّات بچاپ رسیده هشت بار در بمبئی، چهار بار در طهران، سه بار در تبریز، دو بار در کلکته، یک بار در لکنهو، یک بار در دهلی و یک بار هم در خانپور. در خود ایران برای اولین دفعه در شهر تبریز بسال ۱۲۵۷ چاپ شده. گلستان برای اولین بار مدتها قبل از کلیّات بسال ۱۰۶۹ در امستردام چاپ شده ولی تا انوقت ترجمهٔ گلستان یکبار در پاریس بطبع رسیده بود بعد از آن چند بار در کلکته چاپ شد. در ایران برای نخستین مرتبه در سال ۱۲۴۰ در تبرینر چاپ شد. رویهمرفته گلستان ۷۲ مرتبه طبع شده و چاپ اخیر آن (بنظر نگارنده) چاپ برلین است که در مطبعهٔ کاویانی با اسلوب خوبی بطبع رسیده. گلستان ۵۵ مرتبه بالسنهٔ مختلفه ترجمه شده و ترجمه آلمانی ان که از «{{w|فریدریش روزن|دکتر روزن}}»<ref>سابقاً سفیر آلمان در طهران و اخیراً وزیر امور خارجهٔ آلمان بود.</ref> است ترجمه بسیار خوبیست و میتوان گفت که انسان از خواندن بعضی قسمتهای آن باندازهٔ خود اصل محظوظ میشود. کتاب بوستان به تنهائی اولین بار بسال ۱۲۲۹ در کلکته بطبع رسیده ولی مدتی قبل از آن بالسنهٔ خارجی ترجمه شده بود. در ایران نخستین دفعه سال ۱۲۴۷ در {{کذا|تبرینر|تبریز}} چاپ شده و کلّیتهً ۳۴ مرتبه طبع و ۱۷ بار ترجمه شده است. غزلیّات سعدی تا این اواخر جداگانه بطبع نرسیده فقط جزء کلیّات انتشار<noinclude>{{خطکش}} {{پانویس-کوچک}}</noinclude> g5924rhucqhzixh0ika8capl2b9xd8z برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۲ 104 97131 303962 303951 2026-10-11T15:22:17Z Hanooz 17889 /* نمونه‌خوانی‌شده */ 303962 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||(۶)|}}</noinclude>یافته بود ولی بالاخره اهمیت و مقام ادبی این ترانهای شیرین ادبیّات شرقی نظر اهل نظر و مستشرقین را متوجه خود ساخت تا اینکه مقداری از غزلیّات جداگانه منتشر شد. غزلیّات سعدی چهار دسته است: طیّبات، بدایع، خواتیم و غزلیّات قدیمه. عدّهٔ غزلیات طیبات بیشتر از سه قسمت دیگر یعنی ۴۱۸ است در صورتیکه مجموع غزلیات سه جزء دیگر فقط ۲۹۶ میباشد. نسخهٔ حاضره که عبارت از جزء بدایع است ۱۹۰ غزل دارد. اگر در غزلیّات سعدی کاملاً دقت کنیم ملاحظه خواهیم نمود که شاعر بعرفان و تصوّف کاملاً آشناست و از این نقطهٔ نظر اشعار او دارای مقام مهمّی است ولی بعقیدهٔ بعضی در تغزّل بپایهٔ حافظ و در تصوّف بپایهٔ مولوی رومی و عطّار نمیرسد. اما مستشرق انگلیسی «پرفسور براون» در کتاب تاریخ ادبیّات ایران (که بدان اشاره کردیم) در این خصوص مینویسد: {{چپ|{{سه‌نقطه}} in his Odes Saʿdi considered as inferior to no Persian Poet, not even Hafiz.}} در تغزّل سعدی از هیچیک از شعرای ایران حتّی از حافظ هم کمتر نیست. با تمام این احوال غزلیّات سعدی در ادبیات ایران پایهٔ بس بلندی دارد که هر صاحب ذوق سلیم را مجذوب خود مینماید. زحمت انتشار یک سلسله غزلیّات را مرحوم پروفسور «لوکاس کینگ»<ref>معلّم زبان فارسی در دارالفنون دوبلن.</ref> شروع نمود. طیّبات را با اسلوبی که در همین نسخهٔ بدایع ملاحظه میشود سال ۱۹۲۰ در کلکته انتشار داد و زحمت ترتیب و مطابقهٔ نسخهٔ حاضره را متحمل شده غزلیّاترا بزبان انگلیسی نیز ترجمه نمود. متأسفانه چند ماه قبل پس از تمام شدن چاپ بدایع آخرین مراسله که از انگلیس میرسید فوت پروفسور لوکاس کینک را خبر میداد ولی زحمات او از نظر دانش پژوهان محو نشده ذکر خیرش باقی خواهد ماند.<noinclude>{{خطکش}} {{پانویس-کوچک}}</noinclude> 6jegd544ug95chz09kyhhqba6x3hijo 303973 303962 2026-10-11T15:55:24Z Hanooz 17889 303973 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||(۶)|}}</noinclude>یافته بود ولی بالاخره اهمیت و مقام ادبی این ترانهای شیرین ادبیّات شرقی نظر اهل نظر و مستشرقین را متوجه خود ساخت تا اینکه مقداری از غزلیّات جداگانه منتشر شد. غزلیّات سعدی چهار دسته است: طیّبات، بدایع، خواتیم و غزلیّات قدیمه. عدّهٔ غزلیات طیبات بیشتر از سه قسمت دیگر یعنی ۴۱۸ است در صورتیکه مجموع غزلیات سه جزء دیگر فقط ۲۹۶ میباشد. نسخهٔ حاضره که عبارت از جزء بدایع است ۱۹۰ غزل دارد. اگر در غزلیّات سعدی کاملاً دقت کنیم ملاحظه خواهیم نمود که شاعر بعرفان و تصوّف کاملاً آشناست و از این نقطهٔ نظر اشعار او دارای مقام مهمّی است ولی بعقیدهٔ بعضی در تغزّل بپایهٔ حافظ و در تصوّف بپایهٔ مولوی رومی و عطّار نمیرسد. اما مستشرق انگلیسی «پرفسور براون» در کتاب تاریخ ادبیّات ایران (که بدان اشاره کردیم) در این خصوص مینویسد: {{چپ‌چین}}... in his Odes Saʿdi considered as inferior to no Persian Poet, not even Hafiz.{{پایان}} در تغزّل سعدی از هیچیک از شعرای ایران حتّی از حافظ هم کمتر نیست. با تمام این احوال غزلیّات سعدی در ادبیات ایران پایهٔ بس بلندی دارد که هر صاحب ذوق سلیم را مجذوب خود مینماید. زحمت انتشار یک سلسله غزلیّات را مرحوم پروفسور «لوکاس کینگ»<ref>معلّم زبان فارسی در دارالفنون دوبلن.</ref> شروع نمود. طیّبات را با اسلوبی که در همین نسخهٔ بدایع ملاحظه میشود سال ۱۹۲۰ در کلکته انتشار داد و زحمت ترتیب و مطابقهٔ نسخهٔ حاضره را متحمل شده غزلیّاترا بزبان انگلیسی نیز ترجمه نمود. متأسفانه چند ماه قبل پس از تمام شدن چاپ بدایع آخرین مراسله که از انگلیس میرسید فوت پروفسور لوکاس کینک را خبر میداد ولی زحمات او از نظر دانش پژوهان محو نشده ذکر خیرش باقی خواهد ماند.<noinclude>{{خطکش}} {{پانویس-کوچک}}</noinclude> p1l7fqq12isz3ls8ozba81bndl79p7n برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۳ 104 97132 303963 303952 2026-10-11T15:30:56Z Hanooz 17889 /* نمونه‌خوانی‌شده */ 303963 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||(۷)|}}</noinclude>در این کتاب نسخه بدلها بتوسط اشارات نسخ مختلفه در زیر صفحات معیّن شده تا خواننده در صورت داشتن آن از تمام نسخ خطّی و چاپی دیگر بی نیاز باشد و نیز وزن اشعار با تعیین نام عروضی و اشاراتیکه ما بین مستشرقین برای تعیین اوزان اشعار شرقی معمولست بالای هر غزل نوشته شده. ما در زیر علامات و اشارات یادداشتهای زیر صفحات را شرح داده نیز ترجمهٔ بعضی کلمات انگلیسی را که اغلب تکرار میشود مینویسیم.<ref>علت وجود کلمات انگلیسی برای انتشار این کتاب در انگلیس و آمریکاست.</ref> {{وسط|{{اندازه قلم|120%|اشارات بنسخ خطّی}}}} (I): اشارهٔ بنسخهٔ ادارهٔ هندوستان است و ما در مشروحات گذشته از این نسخه اسم بردیم، تاریخ تحریر آن ۷۲۸ هجری (۱۳۲۷ میلادی) است و اشعار ان بترتیب حروف هجا مرتب نشده غزل اوّل طیّبات با قافیهٔ(ی) شروع میشود ولی چنانکه گفتیم متأسفانه این نسخه با وجود اهمیّت و اعتبار زیاد ناقص است و به تنهائی از روی آن نمیتوان رسالهٔ کاملی انتشار داد. (I a): نسخهٔ دیگری از ادارهٔ هندوستان که تاریخ ندارد ولی خیلی قدیم و معتبر است. (I b): باز نسخهٔ قدیمی است از ادارهٔ هندوستان که تاریخ ندارد ولی با وجود قدمت چندان معتبر نیست. (B): نسخهٔ {{w|موزه بریتانیا|موزهٔ بریطانی}} (Britisch Museum) که تاریخ آن ۹۷۴ هجری (۱۵۶۶ میلادی) است و آن اعتبار زیاد ندارد. (P): نسخهٔ {{w|کتابخانه ملی فرانسه|کتابخانهٔ ملی پاریس}} (Bibliotheque Nationale) بتاریخ ۷۷۶ هجری (۱۳۶۵ میلادی) است این نسخه غیر معتبر است و ۸۰ غزل از طیّبات را ندارد و اغلب غزلیّات قسمتهای مختلفه داخل یکدیگر شده است. (S): نسخهٔ {{w|کتابخانه ملی روسیه|کتابخانه شاهی پطرگراد}} (Imperial Library) که نسبت بنسخ سابق الذکر خیلی معتبر است و تاریخ تحریر آن ۷۸۷ هجری (۱۳۸۵ میلادی) است.<noinclude>{{خطکش}} {{پانویس-کوچک}}</noinclude> 2fggkgbrwjp11xxs72z64bvqhwowoip برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۴ 104 97133 303965 303953 2026-10-11T15:35:18Z Hanooz 17889 /* نمونه‌خوانی‌شده */ 303965 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||(۸)|}}</noinclude>(R): نسخهٔ کتابخانهٔ شاهی کابل که در دست پرفسور لوکاس کینگ بوده و این نسخه بخط خوب نوشته شده و عموماً نسخهٔ معتبریست. {{وسط|{{اندازه قلم|120%|اشارات بنسخ چاپی}}}} (T): نسخهٔ طهران (۱۸۵۱ میلادی) (Z): نسخهٔ تبریز (۱۸۴۱) (C): نسخه کلکته (۱۷۹۱) Y(a): بمبئی (۱۸۸۰) Y(h) (۱۴۹۱): (۱۸۹۱) در بمبئی سه نسخه انتشار یافته ولی نسخه اولی با نسخه (Y(a) شبیه است. (D): دهلی (۱۸۵۲) (L) لکنهو انتشار یافته سال ۱۸۷۰، ۱۸۷۶ و ۱۸۸۵. : لکنهو سه نسخه در برای اینکه ایرانیان محتر میکه انگلیسی نمیدانند نیز از مقایسه نسخ مختلفه که در زیر صفحات نوشته شده استفاده نمایند کلمات ذیل را که اغلب تکرار میشود ترجمه مینمائیم. ایضاً تمام = all, و and, هستند = are, ذیل زیر = follows برای = for,,for do = در in, است (مخفف کلمه Manuskript) و نه = = is, سطر =۰ Line نسخه خطی، Ms No, محذوف = ommitted, نسخه چاپی = Text, Text علامت جمع است)، این. This = و در آخر کلمه Ms چاپخانهٔ کاویانی و آفتاب که خدماتش بمعارف ایران فراموش نشدنی است با وجود گرفتاریهای زیاد نسخهٔ حاضرهٔ بدایع و ترجمهٔ انگلیسی آنرا بچاپ رسانده به گنجینهٔ ادبیات ایران میسپارد و امیدوار است که برای تکمیل خدمت بقیّهٔ غزلیّات شاعر را نیز بچاپ رساند. {{وسط|ت. اِرانی معلم ادبیّات شرقی در دارالفنون برلین مهرماه ۱۳۰۴ هجری شمسی }}<noinclude></noinclude> ttod3vhcob3omz9gbx3ts8ia8jsdgig 303968 303965 2026-10-11T15:46:41Z Hanooz 17889 303968 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||(۸)|}}</noinclude>(R): نسخهٔ کتابخانهٔ شاهی کابل که در دست پرفسور لوکاس کینگ بوده و این نسخه بخط خوب نوشته شده و عموماً نسخهٔ معتبریست. {{وسط|{{اندازه قلم|120%|اشارات بنسخ چاپی}}}} (T): نسخهٔ طهران (۱۸۵۱ میلادی) (Z): نسخهٔ تبریز (۱۸۴۱) (C): نسخهٔ کلکته (۱۷۹۱) Y(a): بمبئی (۱۸۸۰) Y(h): {{رچ}}{{ایضاً|بمبئی|»}} (۱۸۹۱) در بمبئی سه نسخه انتشار یافته ولی نسخهٔ اولی با نسخه (Y(a) شبیه است. (D): دهلی (۱۸۵۲) (L): لکنهو، سه نسخه در لکنهو انتشار یافته سال ۱۸۷۰، ۱۸۷۶ و ۱۸۸۵. برای اینکه ایرانیان محترمیکه انگلیسی نمیدانند نیز از مقایسهٔ نسخ مختلفه که در زیر صفحات نوشته شده استفاده نمایند کلمات ذیل را که اغلب تکرار میشود ترجمه مینمائیم. تمام = all, و = and, هستند = are, ذیل زیر = follows, برای = for, ایضاً = for do, در = in, است = is, سطر = Line, نسخهٔ خطی = Ms (مخفّف کلمهٔ Manuskript), نه = No, محذوف = ommitted, نسخهٔ چاپی = Text, (s در آخر کلمه Ms و Text علامت جمع است), این = This. چاپخانهٔ کاویانی و آفتاب که خدماتش بمعارف ایران فراموش نشدنی است با وجود گرفتاریهای زیاد نسخهٔ حاضرهٔ بدایع و ترجمهٔ انگلیسی آنرا بچاپ رسانده به گنجینهٔ ادبیات ایران میسپارد و امیدوار است که برای تکمیل خدمت بقیّهٔ غزلیّات شاعر را نیز بچاپ رساند. {{وسط|ت. اِرانی معلم ادبیّات شرقی در دارالفنون برلین مهرماه ۱۳۰۴ هجری شمسی }}<noinclude></noinclude> lfkpfc8h9xk5k4dl0kaen0iue9qxpjc 303972 303968 2026-10-11T15:51:26Z Hanooz 17889 303972 proofread-page text/x-wiki <noinclude><pagequality level="3" user="Hanooz" />{{سرصفحه‌مستمر||(۸)|}}</noinclude>(R): نسخهٔ {{w|کتابخانه کابل|کتابخانهٔ شاهی کابل}} که در دست پرفسور لوکاس کینگ بوده و این نسخه بخط خوب نوشته شده و عموماً نسخهٔ معتبریست. {{وسط|{{اندازه قلم|120%|اشارات بنسخ چاپی}}}} (T): نسخهٔ طهران (۱۸۵۱ میلادی) (Z): نسخهٔ تبریز (۱۸۴۱) (C): نسخهٔ کلکته (۱۷۹۱) Y(a): بمبئی (۱۸۸۰) Y(h): {{رچ}}{{ایضاً|بمبئی|»}} (۱۸۹۱) در بمبئی سه نسخه انتشار یافته ولی نسخهٔ اولی با نسخه (Y(a) شبیه است. (D): دهلی (۱۸۵۲) (L): لکنهو، سه نسخه در لکنهو انتشار یافته سال ۱۸۷۰، ۱۸۷۶ و ۱۸۸۵. برای اینکه ایرانیان محترمیکه انگلیسی نمیدانند نیز از مقایسهٔ نسخ مختلفه که در زیر صفحات نوشته شده استفاده نمایند کلمات ذیل را که اغلب تکرار میشود ترجمه مینمائیم. تمام = all, و = and, هستند = are, ذیل زیر = follows, برای = for, ایضاً = for do, در = in, است = is, سطر = Line, نسخهٔ خطی = Ms (مخفّف کلمهٔ Manuskript), نه = No, محذوف = ommitted, نسخهٔ چاپی = Text, (s در آخر کلمه Ms و Text علامت جمع است), این = This. چاپخانهٔ کاویانی و آفتاب که خدماتش بمعارف ایران فراموش نشدنی است با وجود گرفتاریهای زیاد نسخهٔ حاضرهٔ بدایع و ترجمهٔ انگلیسی آنرا بچاپ رسانده به گنجینهٔ ادبیات ایران میسپارد و امیدوار است که برای تکمیل خدمت بقیّهٔ غزلیّات شاعر را نیز بچاپ رساند. {{وسط|ت. اِرانی معلم ادبیّات شرقی در دارالفنون برلین مهرماه ۱۳۰۴ هجری شمسی }}<noinclude></noinclude> s2gj3p0pnabd2az53aj4qgiet0h1exv برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۱۵ 104 97134 303974 2026-10-11T16:32:29Z Hanooz 17889 /* نمونه‌خوانی‌نشده */ صفحه‌ای تازه حاوی «{{وسط|'''بدایع شیخ سعدی علیه الرحمه'''}} {{وسط|بسم الله الرحمن الرحیم}} Metre: بسیط مثمن سالم {{شعر|ادامه=بعد|متن= الحمد لله رب العالمین علی ما درّ من نعمة عزّ اسمه و علا الکافل الرّزق<ref></ref> احساناً و موهبةً ان احسنوه و ان لم یحسنوا عملا سبحانه من عظیم...» ایجاد کرد 303974 proofread-page text/x-wiki <noinclude><pagequality level="1" user="Hanooz" />{{سرصفحه‌مستمر||بدایع شیخ سعدی|num}}</noinclude>{{وسط|'''بدایع شیخ سعدی علیه الرحمه'''}} {{وسط|بسم الله الرحمن الرحیم}} Metre: بسیط مثمن سالم {{شعر|ادامه=بعد|متن= الحمد لله رب العالمین علی ما درّ من نعمة عزّ اسمه و علا الکافل الرّزق<ref></ref> احساناً و موهبةً ان احسنوه و ان لم یحسنوا عملا سبحانه من عظیم قادر صمد منشی الوری حبلاً من بعدهم حبلا الجن و الانس و الاکوان جمهرةً تخرّ بین یدیه<ref></ref> سجداً ذللاه<ref></ref> طوبی لطالبه<ref></ref> تعساً<ref></ref> لتارکه بعداً لمتخذ<ref></ref> من دونه بدلا<ref></ref> کم فی البریة<ref></ref> من آثار قدرته و فی السماء لآیات لمن عقلا<ref></ref>}} C, Y(a); . رزق for, راز) YL; X, for r L; e, for : Y (a); , for do: Y (b);, for do. : C, Y (a), Y (b); as, for do: Z;, for do. for, تحیّر C: تخربین یدیه for, مسخرون لدیه اکوان الوان ۱ یدیه forیدیهم (a): تخر o In R this line is omitted. . لطالبه for تارکه) . تعساً for بغیاً متخذ AI (a);, for: C, Y(a); ja, for do. منجزم ۹ In Z lines ۵, ۷, ۸, ۹, ۱۰, ۱۱, are omitted. . for do, و فی البریة (Y (b: کم فی البریة من for, قد ظهرت فی الارض: ۱۰ ۱۱ R; Me, for Mac: C; ie, for do.<noinclude>{{خطکش}} {{پانویس|چپ‌چین=بله}}</noinclude> 3c6cu2hkvklhpupoiw1etkl877592t8 304014 303974 2026-10-11T20:31:20Z Hanooz 17889 304014 proofread-page text/x-wiki <noinclude><pagequality level="1" user="Hanooz" />{{سرصفحه‌مستمر||بدایع شیخ سعدی|num}}</noinclude>{{وسط|'''بدایع شیخ سعدی علیه الرحمه'''}} {{وسط|بسم الله الرحمن الرحیم}} Metre: بسیط مثمن سالم {{شعر۲|ادامه=بعد|متن= الحمد لله رب العالمین علی ما درّ من نعمة عزّ اسمه و علا الکافل الرّزق<ref></ref> احساناً و موهبةً ان احسنوه و ان لم یحسنوا عملا سبحانه من عظیم قادر صمد منشی الوری حبلاً من بعدهم حبلا الجن و الانس و الاکوان جمهرةً تخرّ بین یدیه<ref></ref> سجداً ذللاه<ref></ref> طوبی لطالبه<ref></ref> تعساً<ref></ref> لتارکه بعداً لمتخذ<ref></ref> من دونه بدلا<ref></ref> کم فی البریة<ref></ref> من آثار قدرته و فی السماء لآیات لمن عقلا<ref></ref>}} C, Y(a); . رزق for, راز) YL; X, for r L; e, for : Y (a); , for do: Y (b);, for do. : C, Y (a), Y (b); as, for do: Z;, for do. for, تحیّر C: تخربین یدیه for, مسخرون لدیه اکوان الوان ۱ یدیه forیدیهم (a): تخر o In R this line is omitted. . لطالبه for تارکه) . تعساً for بغیاً متخذ AI (a);, for: C, Y(a); ja, for do. منجزم ۹ In Z lines ۵, ۷, ۸, ۹, ۱۰, ۱۱, are omitted. . for do, و فی البریة (Y (b: کم فی البریة من for, قد ظهرت فی الارض: ۱۰ ۱۱ R; Me, for Mac: C; ie, for do.<noinclude>{{خطکش}} {{پانویس|چپ‌چین=بله}}</noinclude> 2g3peqm8wybj9ul2dyt4vlt1kkznabr بدایع سعدی/مقدمه 0 97135 303975 2026-10-11T16:37:09Z Hanooz 17889 صفحه‌ای تازه حاوی «{{header | title = [[../]] | author = | editor = تقی ارانی | translator = | section = مقدمه | previous = | next = | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=9 to=14></pages> {{خطکش}} {{پانویس-کوچک}}» ایجاد کرد 303975 wikitext text/x-wiki {{header | title = [[../]] | author = | editor = تقی ارانی | translator = | section = مقدمه | previous = | next = | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=9 to=14></pages> {{خطکش}} {{پانویس-کوچک}} dd026s4u72q2g8jh7gma4x7t705ujx7 303976 303975 2026-10-11T16:37:21Z Hanooz 17889 303976 wikitext text/x-wiki {{header | title = [[../]] | author = تقی ارانی | editor = | translator = | section = مقدمه | previous = | next = | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=9 to=14></pages> {{خطکش}} {{پانویس-کوچک}} qs2tkm1gm1q334sfscq707e6jahvi2i بدایع سعدی/شعر شماره ۴ 0 97136 303977 2026-10-11T16:43:56Z Hanooz 17889 صفحه‌ای تازه حاوی «{{header | title = [[../]] | author = سعدی | editor = تقی ارانی | translator = | section = ۴ | previous = [[../شعر شماره ۵|۵]] | next = [[../شعر شماره ۳|۳]] | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=19 to=20 fromsection="p19-20...» ایجاد کرد 303977 wikitext text/x-wiki {{header | title = [[../]] | author = سعدی | editor = تقی ارانی | translator = | section = ۴ | previous = [[../شعر شماره ۵|۵]] | next = [[../شعر شماره ۳|۳]] | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=19 to=20 fromsection="p19-20" tosection="p19-20"></pages> {{خطکش}} {{پانویس|چپ‌چین=بله|اندازه=ریز}} f0pjifg3lnbuh1a7ppitdx7nlo21olk 303978 303977 2026-10-11T16:44:15Z Hanooz 17889 303978 wikitext text/x-wiki {{header | title = [[../]] | author = سعدی | editor = تقی ارانی | translator = | section = ۴ | previous = [[../شعر شماره ۳|۳]] | next = [[../شعر شماره ۵|۵]] | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=19 to=20 fromsection="p19-20" tosection="p19-20"></pages> {{خطکش}} {{پانویس|چپ‌چین=بله|اندازه=ریز}} ghqhrvda9450vh2axauugs0ygtuwao2 پودمان:وزن بدایع سعدی 828 97137 303980 2026-10-11T16:50:13Z Hanooz 17889 ساخته هوش مصنوعی 303980 Scribunto text/plain local p = {} -- Starter lookup table for Persian metrical feet. -- Verify patterns against the prosodic convention used on Persian Wikipedia. local patterns = { ["مفعولن"] = "– – –", ["مفاعیلن"] = "⏑ – – –", ["مفاعلن"] = "⏑ – ⏑ –", ["فعولن"] = "⏑ – –", ["فاعلاتن"] = "– ⏑ – –", ["فاعلن"] = "– ⏑ –", ["فعلاتن"] = "⏑ ⏑ – –", ["فعلن"] = "– –", ["مستفعلن"] = "– – ⏑ –", ["مفتعلن"] = "– – ⏑ –", ["مفعول"] = "– – ⏑", ["فاعلات"] = "– ⏑ – ⏑", ["مفاعیل"] = "⏑ – – ⏑", } function p.main(frame) local args = frame:getParent().args local feet = {} -- Collect all non-empty positional parameters. for i = 1, #args do local foot = mw.text.trim(args[i] or "") if foot ~= "" then table.insert(feet, foot) end end if #feet == 0 then return "" end local root = mw.html.create("span") :attr("dir", "rtl") :css("display", "inline-block") :css("white-space", "nowrap") :css("text-align", "center") :css("font-family", "serif") :css("font-size", "1.2em") -- Opening bracket. root:tag("span") :wikitext("[") -- Render a row of feet separated by vertical bars. local function addRow(values) for i, value in ipairs(values) do if i > 1 then root:tag("span") :css("padding", "0 0.3em") :wikitext("|") end root:tag("span") :css("padding", "0 0.1em") :wikitext(value) end end -- First row: names of the feet. addRow(feet) -- Line break between names and scansion patterns. root:tag("br") -- Second row: corresponding syllabic patterns. local scanned = {} for i, foot in ipairs(feet) do scanned[i] = patterns[foot] or "؟" end addRow(scanned) -- Closing bracket. root:tag("span") :wikitext("]") return tostring(root) end return p oepa7m82xpg2w66yet4e9a8gm8gocl1 303983 303980 2026-10-11T17:15:30Z Hanooz 17889 303983 Scribunto text/plain local p = {} local patterns = { ["مفعولن"] = "– – –", ["مفاعیلن"] = "⏑ – – –", ["مفاعلن"] = "⏑ – ⏑ –", ["فعولن"] = "⏑ – –", ["فاعلاتن"] = "– ⏑ – –", ["فاعلن"] = "– ⏑ –", ["فعلاتن"] = "⏑ ⏑ – –", ["فعلن"] = "– –", ["مستفعلن"] = "– – ⏑ –", ["مفتعلن"] = "– – ⏑ –", ["مفعول"] = "– – ⏑", ["فاعلات"] = "– ⏑ – ⏑", ["مفاعیل"] = "⏑ – – ⏑", } -- Safely collect positional arguments without using the # operator, -- which is unreliable on Scribunto frame argument tables. local function collectPositional(args) local values = {} local i = 1 while true do local v = args[i] if v == nil then break end v = mw.text.trim(v) if v ~= "" then values[#values + 1] = v end i = i + 1 end return values end function p.main(frame) local parent = frame:getParent() local args = (parent and parent.args) or frame.args local feet = collectPositional(args) if #feet == 0 then return "" end local root = mw.html.create("span") :attr("dir", "rtl") :css("display", "inline-block") :css("white-space", "nowrap") :css("text-align", "center") :css("font-family", "serif") :css("font-size", "1.2em") root:tag("span"):wikitext("[") local function addRow(values) for i, value in ipairs(values) do if i > 1 then root:tag("span") :css("padding", "0 0.3em") :wikitext("|") end root:tag("span") :css("padding", "0 0.1em") :wikitext(value) end end addRow(feet) root:tag("br") local scanned = {} for i, foot in ipairs(feet) do scanned[i] = patterns[foot] or "؟" end addRow(scanned) root:tag("span"):wikitext("]") return tostring(root) end return p kckyjr48xdlijxn32gon1iwht11uzql 303984 303983 2026-10-11T17:26:48Z Hanooz 17889 303984 Scribunto text/plain local p = {} local patterns = { ["مفعولن"] = "– – –", ["مفاعیلن"] = "⏑ – – –", ["مفاعلن"] = "⏑ – ⏑ –", ["فعولن"] = "⏑ – –", ["فاعلاتن"] = "– ⏑ – –", ["فاعلن"] = "– ⏑ –", ["فعلاتن"] = "⏑ ⏑ – –", ["فعلن"] = "– –", ["مستفعلن"] = "– – ⏑ –", ["مفتعلن"] = "– – ⏑ –", ["مفعول"] = "– – ⏑", ["فاعلات"] = "– ⏑ – ⏑", ["مفاعیل"] = "⏑ – – ⏑", } local function collectPositional(args) local values = {} local i = 1 while true do local v = args[i] if v == nil then break end v = mw.text.trim(v) if v ~= "" then values[#values + 1] = v end i = i + 1 end return values end function p.main(frame) local parent = frame:getParent() local args = (parent and parent.args) or frame.args local feet = collectPositional(args) if #feet == 0 then return "" end -- Reverse order of appearance. local reversed = {} for i = 1, #feet do reversed[i] = feet[#feet - i + 1] end -- Scansions with internal whitespace stripped. local scanned = {} for i, foot in ipairs(reversed) do local pat = patterns[foot] or "؟" scanned[i] = pat:gsub("%s+", "") end local root = mw.html.create("span") :attr("dir", "rtl") :css("display", "inline-block") :css("white-space", "nowrap") :css("text-align", "center") :css("font-family", "serif") :css("font-size", "1.2em") root:tag("span"):wikitext("[") for i, value in ipairs(scanned) do if i > 1 then -- Bare pipe, no padding around it. root:tag("span"):wikitext("|") end -- letter-spacing keeps adjacent dashes from visually merging, -- negative margin-right cancels the trailing gap so the pipe -- sits flush against the pattern. root:tag("span") :css("letter-spacing", "0.12em") :css("margin-right", "-0.12em") :wikitext(value) end root:tag("span"):wikitext("]") return tostring(root) end return p ay13lrs6n47txekz4sb2xo8hp19weps 303986 303984 2026-10-11T17:34:30Z Hanooz 17889 303986 Scribunto text/plain local p = {} local patterns = { ["مفعولن"] = "– – –", ["مفاعیلن"] = "⏑ – – –", ["مفاعلن"] = "⏑ – ⏑ –", ["فعولن"] = "⏑ – –", ["فاعلاتن"] = "– ⏑ – –", ["فاعلن"] = "– ⏑ –", ["فعلاتن"] = "⏑ ⏑ – –", ["فعلن"] = "– –", ["مستفعلن"] = "– – ⏑ –", ["مفتعلن"] = "– – ⏑ –", ["مفعول"] = "– – ⏑", ["فاعلات"] = "– ⏑ – ⏑", ["مفاعیل"] = "⏑ – – ⏑", } local function collectPositional(args) local values = {} local i = 1 while true do local v = args[i] if v == nil then break end v = mw.text.trim(v) if v ~= "" then values[#values + 1] = v end i = i + 1 end return values end function p.main(frame) local parent = frame:getParent() local args = (parent and parent.args) or frame.args local feet = collectPositional(args) if #feet == 0 then return "" end -- Scansions with internal whitespace stripped. local scanned = {} for i, foot in ipairs(feet) do local pat = patterns[foot] or "؟" scanned[i] = pat:gsub("%s+", "") end local root = mw.html.create("span") :attr("dir", "ltr") :css("display", "inline-block") :css("white-space", "nowrap") :css("text-align", "center") :css("font-family", "serif") :css("font-size", "1.2em") root:tag("span"):wikitext("[") for i, value in ipairs(scanned) do if i > 1 then root:tag("span"):wikitext("|") end root:tag("span") :css("letter-spacing", "0.12em") :css("margin-right", "-0.12em") :wikitext(value) end root:tag("span"):wikitext("]") return tostring(root) end return p hj5h9w7ncpqsikuy0fmok0fuorxietr 303987 303986 2026-10-11T17:56:40Z Hanooz 17889 303987 Scribunto text/plain local p = {} -- Scansion patterns for individual feet. local patterns = { ["مفعولن"] = "– – –", ["مفاعیلن"] = "⏑ – – –", ["مفاعلن"] = "⏑ – ⏑ –", ["فعولن"] = "⏑ – –", ["فاعلاتن"] = "– ⏑ – –", ["فاعلن"] = "– ⏑ –", ["فعلاتن"] = "⏑ ⏑ – –", ["فعلن"] = "– –", ["مستفعلن"] = "– – ⏑ –", ["مفتعلن"] = "– – ⏑ –", ["مفعول"] = "– – ⏑", ["فاعلات"] = "– ⏑ – ⏑", ["مفاعیل"] = "⏑ – – ⏑", } -- Meter names for common foot sequences. -- Key: the feet joined by a space, exactly as they appear in the template call. local meter_names = { -- بحر هزج ["مفاعیلن مفاعیلن مفاعیلن مفاعیلن"] = "هزج مثمن سالم", ["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "هزج مثمن محذوف", ["مفاعیلن مفاعیلن فعولن"] = "هزج مسدس محذوف", ["مفعول مفاعیلن مفعول مفاعیلن"] = "هزج مثمن اخرب", -- بحر رمل ["فاعلاتن فاعلاتن فاعلاتن فاعلاتن"] = "رمل مثمن سالم", ["فاعلاتن فاعلاتن فاعلاتن فاعلن"] = "رمل مثمن محذوف", ["فاعلاتن فاعلاتن فاعلن"] = "رمل مسدس محذوف", ["فعلاتن فعلاتن فعلاتن فعلن"] = "رمل مثمن مخبون محذوف", -- بحر رجز ["مستفعلن مستفعلن مستفعلن مستفعلن"] = "رجز مثمن سالم", ["مستفعلن مستفعلن مستفعلن"] = "رجز مسدس سالم", -- بحر متقارب ["فعولن فعولن فعولن فعولن"] = "متقارب مثمن سالم", ["فعولن فعولن فعولن فعل"] = "متقارب مثمن محذوف", -- بحر مضارع ["مفاعیلن فاعلاتن مفاعیلن فاعلاتن"] = "مضارع مثمن سالم", ["مفعول فاعلاتن مفعول فاعلاتن"] = "مضارع مثمن اخرب", ["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "مضارع مثمن اخرب مکفوف محذوف", -- بحر مجتث ["مفاعلن فعلاتن مفاعلن فعلاتن"] = "مجتث مثمن مخبون", ["مفاعلن فعلاتن مفاعلن فعلن"] = "مجتث مثمن مخبون محذوف", -- بحر خفیف ["فاعلاتن مفاعلن فعلاتن مفاعلن"] = "خفیف مثمن مخبون", ["فاعلاتن مفاعلن فعلن"] = "خفیف مسدس مخبون", } -- Safely collect positional arguments without using the # operator. local function collectPositional(args) local values = {} local i = 1 while true do local v = args[i] if v == nil then break end v = mw.text.trim(v) if v ~= "" then values[#values + 1] = v end i = i + 1 end return values end -- Try to find a meter name for the given list of feet. local function lookupMeterName(feet) local key = table.concat(feet, " ") return meter_names[key] end function p.main(frame) local parent = frame:getParent() local args = (parent and parent.args) or frame.args local feet = collectPositional(args) if #feet == 0 then return "" end -- Scansions with internal whitespace stripped. local scanned = {} for i, foot in ipairs(feet) do local pat = patterns[foot] or "؟" scanned[i] = pat:gsub("%s+", "") end -- Determine the meter name: -- 1. explicit 'وزن' or 'بحر' parameter wins, -- 2. otherwise try the lookup table, -- 3. otherwise leave it blank. local meterName = args["وزن"] or args["بحر"] if not meterName or mw.text.trim(meterName) == "" then meterName = lookupMeterName(feet) end if meterName then meterName = mw.text.trim(meterName) else meterName = "" end -- Main container: centered line, RTL direction so Metre: lands on the left. local container = mw.html.create("div") :css("text-align", "center") :css("direction", "rtl") :css("font-family", "serif") :css("font-size", "1.2em") :css("white-space", "nowrap") -- 1. Notation (LTR) — pinned right in the RTL container. local notationSpan = container:tag("span") :attr("dir", "ltr") :css("display", "inline-block") notationSpan:tag("span"):wikitext("[") for i, value in ipairs(scanned) do if i > 1 then -- Bare pipe, no padding around it. notationSpan:tag("span"):wikitext("|") end -- letter-spacing keeps adjacent dashes from visually merging, -- negative margin-right cancels the trailing gap so the pipe -- sits flush against the pattern. notationSpan:tag("span") :css("letter-spacing", "0.12em") :css("margin-right", "-0.12em") :wikitext(value) end notationSpan:tag("span"):wikitext("]") -- 2. Meter name (RTL) — middle, with a gap on both sides. if meterName ~= "" then container:tag("span") :css("margin", "0 1em") :wikitext(meterName) container:tag("span") :attr("dir", "ltr") :wikitext("Metre:") else -- If no meter name, still provide a gap before Metre:. container:tag("span") :attr("dir", "ltr") :css("margin", "0 1em") :wikitext("Metre:") end return tostring(container) end return p byiso68vtb6irexnwokpustszcgktb0 303988 303987 2026-10-11T18:00:12Z Hanooz 17889 303988 Scribunto text/plain local p = {} -- Scansion patterns for individual feet. local patterns = { ["مفعولن"] = "– – –", ["مفاعیلن"] = "⏑ – – –", ["مفاعلن"] = "⏑ – ⏑ –", ["فعولن"] = "⏑ – –", ["فاعلاتن"] = "– ⏑ – –", ["فاعلن"] = "– ⏑ –", ["فعلاتن"] = "⏑ ⏑ – –", ["فعلن"] = "– –", ["مستفعلن"] = "– – ⏑ –", ["مفتعلن"] = "– – ⏑ –", ["مفعول"] = "– – ⏑", ["فاعلات"] = "– ⏑ – ⏑", ["مفاعیل"] = "⏑ – – ⏑", } -- Meter names for common foot sequences. -- Key: the feet joined by a space, exactly as they appear in the template call. local meter_names = { -- بحر هزج ["مفاعیلن مفاعیلن مفاعیلن مفاعیلن"] = "هزج مثمن سالم", ["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "هزج مثمن محذوف", ["مفاعیلن مفاعیلن فعولن"] = "هزج مسدس محذوف", ["مفعول مفاعیلن مفعول مفاعیلن"] = "هزج مثمن اخرب", -- بحر رمل ["فاعلاتن فاعلاتن فاعلاتن فاعلاتن"] = "رمل مثمن سالم", ["فاعلاتن فاعلاتن فاعلاتن فاعلن"] = "رمل مثمن محذوف", ["فاعلاتن فاعلاتن فاعلن"] = "رمل مسدس محذوف", ["فعلاتن فعلاتن فعلاتن فعلن"] = "رمل مثمن مخبون محذوف", -- بحر رجز ["مستفعلن مستفعلن مستفعلن مستفعلن"] = "رجز مثمن سالم", ["مستفعلن مستفعلن مستفعلن"] = "رجز مسدس سالم", -- بحر متقارب ["فعولن فعولن فعولن فعولن"] = "متقارب مثمن سالم", ["فعولن فعولن فعولن فعل"] = "متقارب مثمن محذوف", -- بحر مضارع ["مفاعیلن فاعلاتن مفاعیلن فاعلاتن"] = "مضارع مثمن سالم", ["مفعول فاعلاتن مفعول فاعلاتن"] = "مضارع مثمن اخرب", ["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "مضارع مثمن اخرب مکفوف محذوف", -- بحر مجتث ["مفاعلن فعلاتن مفاعلن فعلاتن"] = "مجتث مثمن مخبون", ["مفاعلن فعلاتن مفاعلن فعلن"] = "مجتث مثمن مخبون محذوف", -- بحر خفیف ["فاعلاتن مفاعلن فعلاتن مفاعلن"] = "خفیف مثمن مخبون", ["فاعلاتن مفاعلن فعلن"] = "خفیف مسدس مخبون", } -- Safely collect positional arguments without using the # operator. local function collectPositional(args) local values = {} local i = 1 while true do local v = args[i] if v == nil then break end v = mw.text.trim(v) if v ~= "" then values[#values + 1] = v end i = i + 1 end return values end -- Try to find a meter name for the given list of feet. local function lookupMeterName(feet) local key = table.concat(feet, " ") return meter_names[key] end function p.main(frame) local parent = frame:getParent() local args = (parent and parent.args) or frame.args local feet = collectPositional(args) if #feet == 0 then return "" end -- Scansions with internal whitespace stripped. local scanned = {} for i, foot in ipairs(feet) do local pat = patterns[foot] or "؟" scanned[i] = pat:gsub("%s+", "") end -- Determine the meter name: -- 1. explicit 'وزن' or 'بحر' parameter wins, -- 2. otherwise try the lookup table, -- 3. otherwise leave it blank. local meterName = args["وزن"] or args["بحر"] if not meterName or mw.text.trim(meterName) == "" then meterName = lookupMeterName(feet) end if meterName then meterName = mw.text.trim(meterName) else meterName = "" end -- Main container: centered line, RTL direction so Metre: lands on the left. -- No font-family / font-size overrides — inherits normal wiki text style. local container = mw.html.create("div") :css("text-align", "center") :css("direction", "rtl") :css("white-space", "nowrap") -- 1. Notation (LTR) — pinned right in the RTL container. local notationSpan = container:tag("span") :attr("dir", "ltr") :css("display", "inline-block") notationSpan:tag("span"):wikitext("[") for i, value in ipairs(scanned) do if i > 1 then -- Bare pipe, no padding around it. notationSpan:tag("span"):wikitext("|") end -- letter-spacing keeps adjacent dashes from visually merging, -- negative margin-right cancels the trailing gap so the pipe -- sits flush against the pattern. notationSpan:tag("span") :css("letter-spacing", "0.12em") :css("margin-right", "-0.12em") :wikitext(value) end notationSpan:tag("span"):wikitext("]") -- 2. Meter name (RTL) — middle. -- In the RTL container, the right neighbour of this span is the -- notation, and the left neighbour is the 'Metre:' label. -- So: margin-right = big gap (notation), margin-left = small gap (Metre:). if meterName ~= "" then container:tag("span") :css("margin-right", "2em") -- gap toward notation :css("margin-left", "0.4em") -- gap toward Metre: :wikitext(meterName) container:tag("span") :attr("dir", "ltr") :wikitext("Metre:") else -- No meter name: just leave a gap before Metre:. container:tag("span") :attr("dir", "ltr") :css("margin-right", "2em") :wikitext("Metre:") end return tostring(container) end return p bt18w7ii02kof79vfq49wl8pn58d3qp 303989 303988 2026-10-11T18:03:15Z Hanooz 17889 303989 Scribunto text/plain local p = {} -- Scansion patterns for individual feet. local patterns = { ["مفعولن"] = "– – –", ["مفاعیلن"] = "⏑ – – –", ["مفاعلن"] = "⏑ – ⏑ –", ["فعولن"] = "⏑ – –", ["فاعلاتن"] = "– ⏑ – –", ["فاعلن"] = "– ⏑ –", ["فعلاتن"] = "⏑ ⏑ – –", ["فعلن"] = "– –", ["مستفعلن"] = "– – ⏑ –", ["مفتعلن"] = "– – ⏑ –", ["مفعول"] = "– – ⏑", ["فاعلات"] = "– ⏑ – ⏑", ["مفاعیل"] = "⏑ – – ⏑", } -- Meter names for common foot sequences. -- Key: the feet joined by a space, exactly as they appear in the template call. local meter_names = { -- بحر هزج ["مفاعیلن مفاعیلن مفاعیلن مفاعیلن"] = "هزج مثمّن سالم", ["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "هزج مثمّن محذوف", ["مفاعیلن مفاعیلن فعولن"] = "هزج مسدّس محذوف", ["مفعول مفاعیلن مفعول مفاعیلن"] = "هزج مثمّن اخرب", -- بحر رمل ["فاعلاتن فاعلاتن فاعلاتن فاعلاتن"] = "رمل مثمّن سالم", ["فاعلاتن فاعلاتن فاعلاتن فاعلن"] = "رمل مثمّن محذوف", ["فاعلاتن فاعلاتن فاعلن"] = "رمل مسدّس محذوف", ["فعلاتن فعلاتن فعلاتن فعلن"] = "رمل مثمّن مخبون محذوف", -- بحر رجز ["مستفعلن مستفعلن مستفعلن مستفعلن"] = "رجز مثمّن سالم", ["مستفعلن مستفعلن مستفعلن"] = "رجز مسدّس سالم", -- بحر متقارب ["فعولن فعولن فعولن فعولن"] = "متقارب مثمّن سالم", ["فعولن فعولن فعولن فعل"] = "متقارب مثمّن محذوف", -- بحر مضارع ["مفاعیلن فاعلاتن مفاعیلن فاعلاتن"] = "مضارع مثمّن سالم", ["مفعول فاعلاتن مفعول فاعلاتن"] = "مضارع مثمّن اخرب", ["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "مضارع مثمّن اخرب مکفوف محذوف", -- بحر مجتث ["مفاعلن فعلاتن مفاعلن فعلاتن"] = "مجتثّ مثمّن مخبون", ["مفاعلن فعلاتن مفاعلن فعلن"] = "مجتثّ مثمّن مخبون محذوف", -- بحر خفیف ["فاعلاتن مفاعلن فعلاتن مفاعلن"] = "خفیف مثمّن مخبون", ["فاعلاتن مفاعلن فعلن"] = "خفیف مسدّس مخبون", } -- Safely collect positional arguments without using the # operator. local function collectPositional(args) local values = {} local i = 1 while true do local v = args[i] if v == nil then break end v = mw.text.trim(v) if v ~= "" then values[#values + 1] = v end i = i + 1 end return values end -- Try to find a meter name for the given list of feet. local function lookupMeterName(feet) local key = table.concat(feet, " ") return meter_names[key] end function p.main(frame) local parent = frame:getParent() local args = (parent and parent.args) or frame.args local feet = collectPositional(args) if #feet == 0 then return "" end -- Scansions with internal whitespace stripped. local scanned = {} for i, foot in ipairs(feet) do local pat = patterns[foot] or "؟" scanned[i] = pat:gsub("%s+", "") end -- Determine the meter name: -- 1. explicit 'وزن' or 'بحر' parameter wins, -- 2. otherwise try the lookup table, -- 3. otherwise leave it blank. local meterName = args["وزن"] or args["بحر"] if not meterName or mw.text.trim(meterName) == "" then meterName = lookupMeterName(feet) end if meterName then meterName = mw.text.trim(meterName) else meterName = "" end -- Main container: centered line, RTL direction so Metre: lands on the left. -- No font-family / font-size overrides — inherits normal wiki text style. local container = mw.html.create("div") :css("text-align", "center") :css("direction", "rtl") :css("white-space", "nowrap") -- 1. Notation (LTR) — pinned right in the RTL container. local notationSpan = container:tag("span") :attr("dir", "ltr") :css("display", "inline-block") notationSpan:tag("span"):wikitext("[") for i, value in ipairs(scanned) do if i > 1 then -- Bare pipe, no padding around it. notationSpan:tag("span"):wikitext("|") end -- letter-spacing keeps adjacent dashes from visually merging, -- negative margin-right cancels the trailing gap so the pipe -- sits flush against the pattern. notationSpan:tag("span") :css("letter-spacing", "0.12em") :css("margin-right", "-0.12em") :wikitext(value) end notationSpan:tag("span"):wikitext("]") -- 2. Meter name (RTL) — middle. -- In the RTL container, the right neighbour of this span is the -- notation, and the left neighbour is the 'Metre:' label. -- So: margin-right = big gap (notation), margin-left = small gap (Metre:). if meterName ~= "" then container:tag("span") :css("margin-right", "2em") -- gap toward notation :css("margin-left", "0.4em") -- gap toward Metre: :wikitext(meterName) container:tag("span") :attr("dir", "ltr") :wikitext("Metre:") else -- No meter name: just leave a gap before Metre:. container:tag("span") :attr("dir", "ltr") :css("margin-right", "2em") :wikitext("Metre:") end return tostring(container) end return p oafqys95fyh52x1yqb9ht1bxm1jcel8 303990 303989 2026-10-11T18:08:34Z Hanooz 17889 303990 Scribunto text/plain local p = {} -- Scansion patterns for individual feet. local patterns = { ["مفعولن"] = "– – –", ["مفاعیلن"] = "⏑ – – –", ["مفاعلن"] = "⏑ – ⏑ –", ["فعولن"] = "⏑ – –", ["فاعلاتن"] = "– ⏑ – –", ["فاعلن"] = "– ⏑ –", ["فعلاتن"] = "⏑ ⏑ – –", ["فعلن"] = "– –", ["مستفعلن"] = "– – ⏑ –", ["مفتعلن"] = "– – ⏑ –", ["مفعول"] = "– – ⏑", ["فاعلات"] = "– ⏑ – ⏑", ["مفاعیل"] = "⏑ – – ⏑", } -- Meter names for common foot sequences. -- Key: the feet joined by a space, exactly as they appear in the template call. local meter_names = { -- بحر هزج ["مفاعیلن مفاعیلن مفاعیلن مفاعیلن"] = "هزج مثمّن سالم", ["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "هزج مثمّن محذوف", ["مفاعیلن مفاعیلن فعولن"] = "هزج مسدّس محذوف", ["مفعول مفاعیلن مفعول مفاعیلن"] = "هزج مثمّن اخرب", -- بحر رمل ["فاعلاتن فاعلاتن فاعلاتن فاعلاتن"] = "رمل مثمّن سالم", ["فاعلاتن فاعلاتن فاعلاتن فاعلن"] = "رمل مثمّن محذوف", ["فاعلاتن فاعلاتن فاعلن"] = "رمل مسدّس محذوف", ["فعلاتن فعلاتن فعلاتن فعلن"] = "رمل مثمّن مخبون محذوف", -- بحر رجز ["مستفعلن مستفعلن مستفعلن مستفعلن"] = "رجز مثمّن سالم", ["مستفعلن مستفعلن مستفعلن"] = "رجز مسدّس سالم", -- بحر متقارب ["فعولن فعولن فعولن فعولن"] = "متقارب مثمّن سالم", ["فعولن فعولن فعولن فعل"] = "متقارب مثمّن محذوف", -- بحر مضارع ["مفاعیلن فاعلاتن مفاعیلن فاعلاتن"] = "مضارع مثمّن سالم", ["مفعول فاعلاتن مفعول فاعلاتن"] = "مضارع مثمّن اخرب", ["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "مضارع مثمّن اخرب مکفوف محذوف", -- بحر مجتث ["مفاعلن فعلاتن مفاعلن فعلاتن"] = "مجتثّ مثمّن مخبون", ["مفاعلن فعلاتن مفاعلن فعلن"] = "مجتثّ مثمّن مخبون محذوف", -- بحر خفیف ["فاعلاتن مفاعلن فعلاتن مفاعلن"] = "خفیف مثمّن مخبون", ["فاعلاتن مفاعلن فعلن"] = "خفیف مسدّس مخبون", } -- Safely collect positional arguments without using the # operator. local function collectPositional(args) local values = {} local i = 1 while true do local v = args[i] if v == nil then break end v = mw.text.trim(v) if v ~= "" then values[#values + 1] = v end i = i + 1 end return values end -- Try to find a meter name for the given list of feet. local function lookupMeterName(feet) local key = table.concat(feet, " ") return meter_names[key] end function p.main(frame) local parent = frame:getParent() local args = (parent and parent.args) or frame.args local feet = collectPositional(args) if #feet == 0 then return "" end -- Scansions with internal whitespace stripped. local scanned = {} for i, foot in ipairs(feet) do local pat = patterns[foot] or "؟" scanned[i] = pat:gsub("%s+", "") end -- Determine the meter name: -- 1. explicit 'وزن' or 'بحر' parameter wins, -- 2. otherwise try the lookup table, -- 3. otherwise leave it blank. local meterName = args["وزن"] or args["بحر"] if not meterName or mw.text.trim(meterName) == "" then meterName = lookupMeterName(feet) end if meterName then meterName = mw.text.trim(meterName) else meterName = "" end -- Main container: centered line, RTL direction so Metre: lands on the left. -- No font-family / font-size overrides — inherits normal wiki text style. local container = mw.html.create("div") :css("text-align", "center") :css("direction", "rtl") :css("white-space", "nowrap") -- 1. Notation (LTR) — pinned right in the RTL container. local notationSpan = container:tag("span") :attr("dir", "ltr") :css("display", "inline-block") notationSpan:tag("span"):wikitext("[") for i, value in ipairs(scanned) do if i > 1 then -- Bare pipe, no padding around it. notationSpan:tag("span"):wikitext("|") end -- letter-spacing keeps adjacent dashes from visually merging, -- negative margin-right cancels the trailing gap so the pipe -- sits flush against the pattern. notationSpan:tag("span") :css("letter-spacing", "0.12em") :css("margin-right", "-0.12em") :wikitext(value) end notationSpan:tag("span"):wikitext("]") -- 2. Meter name (RTL) — middle. -- In the RTL container, the right neighbour of this span is the -- notation, and the left neighbour is the 'Metre:' label. -- So: margin-right = big gap (notation), margin-left = small gap (Metre:). if meterName ~= "" then container:tag("span") :css("margin-right", "2em") -- gap toward notation :css("margin-left", "0.4em") -- gap toward Metre: :wikitext(meterName) container:tag("span") :attr("dir", "ltr") :wikitext("Metre:") else -- No meter name: just leave a gap before Metre:. container:tag("span") :attr("dir", "ltr") :css("margin-right", "2em") :wikitext("Metre:") end local output = tostring(container) -- Auto-categorize, but only on pages in the main namespace. -- The category is named after the feet parameters, joined by spaces, -- e.g. [[رده:فاعلاتن فاعلاتن فاعلاتن فاعلاتن]]. local title = mw.title.getCurrentTitle() if title and title.namespace == 0 then local catName = table.concat(feet, " ") output = output .. "[[رده:" .. catName .. "]]" end return output end return p 3jehiggcv0ulr77n5pwfqwho6ur5o4z 303994 303990 2026-10-11T18:23:08Z Hanooz 17889 303994 Scribunto text/plain -- نمایش ارکان و الگوی هجایی وزن شعر فارسی همراه با نام وزن و ردهٔ خودکار. local p = {} -- الگوی هجایی هر رکن. local patterns = { ["مفعولن"] = "– – –", ["مفاعیلن"] = "⏑ – – –", ["مفاعلن"] = "⏑ – ⏑ –", ["فعولن"] = "⏑ – –", ["فاعلاتن"] = "– ⏑ – –", ["فاعلن"] = "– ⏑ –", ["فعلاتن"] = "⏑ ⏑ – –", ["فعلن"] = "– –", ["مستفعلن"] = "– – ⏑ –", ["مفتعلن"] = "– – ⏑ –", ["مفعول"] = "– – ⏑", ["فاعلات"] = "– ⏑ – ⏑", ["مفاعیل"] = "⏑ – – ⏑", } -- نام وزن بر اساس رشتهٔ ارکان. -- کلید: ارکان با فاصله از هم جدا شده، دقیقاً همان‌طور که به الگو داده می‌شوند. local meter_names = { -- بحر هزج ["مفاعیلن مفاعیلن مفاعیلن مفاعیلن"] = "هزج مثمّن سالم", ["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "هزج مثمّن محذوف", ["مفاعیلن مفاعیلن فعولن"] = "هزج مسدّس محذوف", ["مفعول مفاعیلن مفعول مفاعیلن"] = "هزج مثمّن اخرب", -- بحر رمل ["فاعلاتن فاعلاتن فاعلاتن فاعلاتن"] = "رمل مثمّن سالم", ["فاعلاتن فاعلاتن فاعلاتن فاعلن"] = "رمل مثمّن محذوف", ["فاعلاتن فاعلاتن فاعلن"] = "رمل مسدّس محذوف", ["فعلاتن فعلاتن فعلاتن فعلن"] = "رمل مثمّن مخبون محذوف", -- بحر رجز ["مستفعلن مستفعلن مستفعلن مستفعلن"] = "رجز مثمّن سالم", ["مستفعلن مستفعلن مستفعلن"] = "رجز مسدّس سالم", -- بحر متقارب ["فعولن فعولن فعولن فعولن"] = "متقارب مثمّن سالم", ["فعولن فعولن فعولن فعل"] = "متقارب مثمّن محذوف", -- بحر مضارع ["مفاعیلن فاعلاتن مفاعیلن فاعلاتن"] = "مضارع مثمّن سالم", ["مفعول فاعلاتن مفعول فاعلاتن"] = "مضارع مثمّن اخرب", ["مفاعیلن مفاعیلن مفاعیلن فعولن"] = "مضارع مثمّن اخرب مکفوف محذوف", -- بحر مجتث ["مفاعلن فعلاتن مفاعلن فعلاتن"] = "مجتثّ مثمّن مخبون", ["مفاعلن فعلاتن مفاعلن فعلن"] = "مجتثّ مثمّن مخبون محذوف", -- بحر خفیف ["فاعلاتن مفاعلن فعلاتن مفاعلن"] = "خفیف مثمّن مخبون", ["فاعلاتن مفاعلن فعلن"] = "خفیف مسدّس مخبون", } -- گردآوری پارامترهای مکانی بدون استفاده از عملگر # (که روی جدول‌های -- فریم اسکریبونتو قابل اعتماد نیست). local function collectPositional(args) local values = {} local i = 1 while true do local v = args[i] if v == nil then break end v = mw.text.trim(v) if v ~= "" then values[#values + 1] = v end i = i + 1 end return values end -- جستجوی نام وزن بر اساس فهرست ارکان. local function lookupMeterName(feet) local key = table.concat(feet, " ") return meter_names[key] end function p.main(frame) local parent = frame:getParent() local args = (parent and parent.args) or frame.args local feet = collectPositional(args) if #feet == 0 then return "" end -- الگوهای هجایی با حذف فاصله‌های داخلی. local scanned = {} for i, foot in ipairs(feet) do local pat = patterns[foot] or "؟" scanned[i] = pat:gsub("%s+", "") end -- تعیین نام وزن: -- ۱. پارامتر صریح «وزن» یا «بحر» اولویت دارد، -- ۲. در غیر این صورت جدول جستجو بررسی می‌شود، -- ۳. در غیر این صورت خالی می‌ماند. local meterName = args["وزن"] or args["بحر"] if not meterName or mw.text.trim(meterName) == "" then meterName = lookupMeterName(feet) end if meterName then meterName = mw.text.trim(meterName) else meterName = "" end -- بارگذاری شیوه‌نامه از زیرصفحهٔ styles.css. local styles = frame:extensionTag { name = "templatestyles", args = { src = "Module:وزن بدایع سعدی/styles.css" }, } -- ظرف اصلی. local container = mw.html.create("div"):addClass("wazn-container") -- ۱. بخش نشانه‌گذاری (چپ‌به‌راست) — در ظرف راست‌به‌چپ در سمت راست می‌نشیند. local notationSpan = container:tag("span"):addClass("wazn-notation") notationSpan:tag("span"):wikitext("[") for i, value in ipairs(scanned) do if i > 1 then notationSpan:tag("span"):addClass("wazn-pipe"):wikitext("|") end notationSpan:tag("span"):addClass("wazn-pattern"):wikitext(value) end notationSpan:tag("span"):wikitext("]") -- ۲. نام وزن (راست‌به‌چپ) و برچسب Metre: در سمت چپ. if meterName ~= "" then container:tag("span") :addClass("wazn-meter") :wikitext(meterName) container:tag("span") :addClass("wazn-metre-label") :wikitext("Metre:") else container:tag("span") :addClass("wazn-metre-label") :addClass("wazn-metre-label-spaced") :wikitext("Metre:") end local output = styles .. tostring(container) -- ردهٔ خودکار فقط در فضای نام اصلی (نه صفحه: و نه هیچ فضای نام دیگری). local title = mw.title.getCurrentTitle() if title and title.namespace == 0 then local catName = table.concat(feet, " ") output = output .. "[[رده:" .. catName .. "]]" end return output end return p mte3aattvpliru9no5mcbs9dwg9yj2m الگو:وزن بدایع سعدی 10 97138 303981 2026-10-11T16:51:08Z Hanooz 17889 صفحه‌ای تازه حاوی « <includeonly>{{#invoke:وزن بدایع سعدی|main}}</includeonly><noinclude> برای نمایش ارکان و الگوی هجایی وزن شعر فارسی. </noinclude>» ایجاد کرد 303981 wikitext text/x-wiki <includeonly>{{#invoke:وزن بدایع سعدی|main}}</includeonly><noinclude> برای نمایش ارکان و الگوی هجایی وزن شعر فارسی. </noinclude> 7ifuj7ml3qenugnx2sc5t3464jz1x39 303996 303981 2026-10-11T18:26:24Z Hanooz 17889 303996 wikitext text/x-wiki <includeonly>{{#invoke:وزن بدایع سعدی|main}}</includeonly><noinclude> {{مستندات}}</noinclude> 5m8t6eowa38irlnr66mfs18klagp9a9 پودمان:وزن بدایع سعدی/styles.css 828 97139 303993 2026-10-11T18:21:02Z Hanooz 17889 صفحه‌ای تازه حاوی «/* ظرف اصلی: خط وسط‌چین، جهت راست‌به‌چپ برای قرار گرفتن Metre: در چپ. */ .wazn-container { text-align: center; direction: rtl; white-space: nowrap; } /* بخش نشانه‌گذاری — جهت چپ‌به‌راست تا ترتیب نویسه‌ها به‌هم نریزد. */ .wazn-notation { display: inline-block; direction: ltr; unicode-bidi: isolate; } /*...» ایجاد کرد 303993 sanitized-css text/css /* ظرف اصلی: خط وسط‌چین، جهت راست‌به‌چپ برای قرار گرفتن Metre: در چپ. */ .wazn-container { text-align: center; direction: rtl; white-space: nowrap; } /* بخش نشانه‌گذاری — جهت چپ‌به‌راست تا ترتیب نویسه‌ها به‌هم نریزد. */ .wazn-notation { display: inline-block; direction: ltr; unicode-bidi: isolate; } /* هر الگوی هجایی: فاصلهٔ نویسه‌ای تا خط‌های پیوسته به‌هم نچسبند. */ .wazn-pattern { letter-spacing: 0.12em; margin-right: -0.12em; } /* میلهٔ جداکننده با فاصلهٔ صفر از الگوها. */ .wazn-pipe { /* عمداً خالی — فضای پیش‌فرض صفر است. */ } /* نام وزن: فاصلهٔ بزرگ تا نشانه‌گذاری، فاصلهٔ کوچک تا Metre:. */ .wazn-meter { margin-right: 2em; margin-left: 0.4em; } /* برچسب Metre: */ .wazn-metre-label { direction: ltr; unicode-bidi: isolate; } /* حالت بدون نام وزن: تنها فاصلهٔ بزرگ پیش از Metre:. */ .wazn-metre-label-spaced { margin-right: 2em; } iqv2264nyq6jfva4ec5a9yj6jbwt8br 304029 303993 2026-10-11T22:16:22Z Hanooz 17889 304029 sanitized-css text/css /* ظرف اصلی: خط وسط‌چین، جهت راست‌به‌چپ برای قرار گرفتن Metre: در چپ. */ .wazn-container { text-align: center; direction: rtl; white-space: nowrap; } /* بخش نشانه‌گذاری — جهت چپ‌به‌راست تا ترتیب نویسه‌ها به‌هم نریزد. */ .wazn-notation { display: inline-block; direction: ltr; unicode-bidi: isolate; } /* هر الگوی هجایی: فاصلهٔ نویسه‌ای تا خط‌های پیوسته به‌هم نچسبند. */ .wazn-pattern { letter-spacing: 0.12em; margin-right: -0.12em; } /* میلهٔ جداکننده با فاصلهٔ صفر از الگوها. */ .wazn-pipe { /* عمداً خالی. */ } /* نام وزن: فاصلهٔ بزرگ تا نشانه‌گذاری، فاصلهٔ کوچک تا Metre:. */ .wazn-meter { margin-right: 2em; margin-left: 0.4em; } /* برچسب Metre: */ .wazn-metre-label { direction: ltr; unicode-bidi: isolate; } /* حالت بدون نام وزن. */ .wazn-metre-label-spaced { margin-right: 2em; } /* ── صفحه‌های کوچک ───────────────────────────────────────────── وقتی خط می‌شکند، ترتیب از چپ به راست چنین می‌شود: Metre: → نام وزن → نشانه‌گذاری یعنی Metre: روی خط اول می‌ماند و نشانه‌گذاری در صورت نبود جا به خط بعد می‌رود. ─────────────────────────────────────────────────────────── */ @media (max-width: 600px) { .wazn-container { display: flex; flex-wrap: wrap; justify-content: center; align-items: baseline; /* برای ترتیب طبیعی چپ‌به‌راست، در صفحه‌های کوچک ظرف را LTR می‌کنیم. */ direction: ltr; white-space: normal; row-gap: 0.25em; } /* ۱. Metre: — چپ‌ترین، همیشه روی خط اول. */ .wazn-metre-label, .wazn-metre-label-spaced { order: 1; margin: 0; } /* ۲. نام وزن — کنار Metre:، در صورت طولانی بودن می‌شکند. */ .wazn-meter { order: 2; margin: 0 0 0 0.5em; white-space: normal; } /* ۳. نشانه‌گذاری — آخرین مورد؛ اگر جا نباشد به خط بعد می‌رود. */ .wazn-notation { order: 3; } } kjupcwehzvarfs4xyp5efmugfnsip5f الگو:وزن بدایع سعدی/توضیحات 10 97140 303995 2026-10-11T18:25:48Z Hanooz 17889 صفحه‌ای تازه حاوی «<noinclude>{{توضیحات بالای صفحه}}</noinclude> این ماژول وزن عروضی شعر فارسی را به‌صورت یک خط وسط‌چین نشان می‌دهد که سه بخش دارد: <div dir="ltr" style="text-align:center; font-family:monospace; margin:1em 0;"> [–⏑––|–⏑––|–⏑––|–⏑––]&nbsp;&nbsp;&nbsp;&nbsp;مضارع مثمّن اخرب مکفوف محذوف&nbsp;&nbsp;&nb...» ایجاد کرد 303995 wikitext text/x-wiki <noinclude>{{توضیحات بالای صفحه}}</noinclude> این ماژول وزن عروضی شعر فارسی را به‌صورت یک خط وسط‌چین نشان می‌دهد که سه بخش دارد: <div dir="ltr" style="text-align:center; font-family:monospace; margin:1em 0;"> [–⏑––|–⏑––|–⏑––|–⏑––]&nbsp;&nbsp;&nbsp;&nbsp;مضارع مثمّن اخرب مکفوف محذوف&nbsp;&nbsp;&nbsp;&nbsp;Metre: </div> از راست به چپ: '''نشانه‌گذاری هجایی''' — '''نام وزن''' — '''برچسب Metre:'''. == نحوهٔ فراخوانی == این ماژول از طریق الگوی {{الگو|وزن بدایع سعدی}} فراخوانی می‌شود. الگو تنها یک خط دارد: <pre> &lt;includeonly&gt;{{#invoke:وزن بدایع سعدی|main}}&lt;/includeonly&gt;&lt;noinclude&gt; {{توضیحات|الگو:وزن بدایع سعدی/توضیحات}} &lt;/noinclude&gt; </pre> سپس در صفحه‌های محتوا: <pre> {{وزن بدایع سعدی|مفاعیلن|مفاعیلن|مفاعیلن|فعولن}} </pre> == پارامترها == {| class="wikitable" ! پارامتر !! نوع !! توضیح |- | <code>1</code>, <code>2</code>, <code>3</code>, … || مکانی || نام ارکان وزن به ترتیب ظهور در مصراع. |- | <code>وزن</code> یا <code>بحر</code> || نام‌دار (اختیاری) || نام وزن. در صورت نبود، ماژول خودش از جدول جستجو پیدا می‌کند. |} هر پارامتر مکانی خالی نادیده گرفته می‌شود؛ می‌توانید فاصله یا خط جدید هم اضافه کنید. == ارکان شناخته‌شده == {| class="wikitable" style="text-align:center;" ! رکن !! الگوی هجایی |- | مفعولن || <code>–––</code> |- | مفاعیلن || <code>⏑–––</code> |- | مفاعلن || <code>⏑–⏑–</code> |- | فعولن || <code>⏑––</code> |- | فاعلاتن || <code>–⏑––</code> |- | فاعلن || <code>–⏑–</code> |- | فعلاتن || <code>⏑⏑––</code> |- | فعلن || <code>––</code> |- | مستفعلن || <code>––⏑–</code> |- | مفتعلن || <code>––⏑–</code> |- | مفعول || <code>––⏑</code> |- | فاعلات || <code>–⏑–⏑</code> |- | مفاعیل || <code>⏑––⏑</code> |} اگر رکنی در جدول نباشد، به‌جای الگوی هجایی <code>؟</code> چاپ می‌شود. == وزن‌های شناخته‌شده == ماژول این ترکیب‌های ارکان را به‌طور خودکار می‌شناسد و نام وزن را نمایش می‌دهد: === بحر هزج === * <code>مفاعیلن مفاعیلن مفاعیلن مفاعیلن</code> ← هزج مثمّن سالم * <code>مفاعیلن مفاعیلن مفاعیلن فعولن</code> ← هزج مثمّن محذوف * <code>مفاعیلن مفاعیلن فعولن</code> ← هزج مسدّس محذوف * <code>مفعول مفاعیلن مفعول مفاعیلن</code> ← هزج مثمّن اخرب === بحر رمل === * <code>فاعلاتن فاعلاتن فاعلاتن فاعلاتن</code> ← رمل مثمّن سالم * <code>فاعلاتن فاعلاتن فاعلاتن فاعلن</code> ← رمل مثمّن محذوف * <code>فاعلاتن فاعلاتن فاعلن</code> ← رمل مسدّس محذوف * <code>فعلاتن فعلاتن فعلاتن فعلن</code> ← رمل مثمّن مخبون محذوف === بحر رجز === * <code>مستفعلن مستفعلن مستفعلن مستفعلن</code> ← رجز مثمّن سالم * <code>مستفعلن مستفعلن مستفعلن</code> ← رجز مسدّس سالم === بحر متقارب === * <code>فعولن فعولن فعولن فعولن</code> ← متقارب مثمّن سالم * <code>فعولن فعولن فعولن فعل</code> ← متقارب مثمّن محذوف === بحر مضارع === * <code>مفاعیلن فاعلاتن مفاعیلن فاعلاتن</code> ← مضارع مثمّن سالم * <code>مفعول فاعلاتن مفعول فاعلاتن</code> ← مضارع مثمّن اخرب * <code>مفاعیلن مفاعیلن مفاعیلن فعولن</code> ← مضارع مثمّن اخرب مکفوف محذوف === بحر مجتث === * <code>مفاعلن فعلاتن مفاعلن فعلاتن</code> ← مجتثّ مثمّن مخبون * <code>مفاعلن فعلاتن مفاعلن فعلن</code> ← مجتثّ مثمّن مخبون محذوف === بحر خفیف === * <code>فاعلاتن مفاعلن فعلاتن مفاعلن</code> ← خفیف مثمّن مخبون * <code>فاعلاتن مفاعلن فعلن</code> ← خفیف مسدّس مخبون == مثال‌های واقعی == === ۱. حافظ — «دلا بسوز که سوز تو کارها بکند» === وزن: '''هزج مسدّس محذوف''' <pre> {{وزن بدایع سعدی|مفاعیلن|مفاعیلن|فعولن}} </pre> خروجی: <div dir="ltr" style="text-align:center; font-family:monospace; margin:0.5em 0;"> [⏑–––|⏑–––|⏑––]&nbsp;&nbsp;&nbsp;&nbsp;هزج مسدّس محذوف&nbsp;&nbsp;&nbsp;&nbsp;Metre: </div> === ۲. سعدی — «بنی آدم اعضای یک پیکرند» === وزن: '''رمل مسدّس محذوف''' <pre> {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلن}} </pre> خروجی: <div dir="ltr" style="text-align:center; font-family:monospace; margin:0.5em 0;"> [–⏑––|–⏑––|–⏑–]&nbsp;&nbsp;&nbsp;&nbsp;رمل مسدّس محذوف&nbsp;&nbsp;&nbsp;&nbsp;Metre: </div> === ۳. مولوی — «بشنو از نی چون حکایت می‌کند» === همان وزن بالا: <pre> {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلن}} </pre> === ۴. حافظ — «دوش دیدم که ملائک در میخانه زدند» === وزن: '''رمل مثمّن مخبون محذوف''' <pre> {{وزن بدایع سعدی|فعلاتن|فعلاتن|فعلاتن|فعلن}} </pre> خروجی: <div dir="ltr" style="text-align:center; font-family:monospace; margin:0.5em 0;"> [⏑⏑––|⏑⏑––|⏑⏑––|––]&nbsp;&nbsp;&nbsp;&nbsp;رمل مثمّن مخبون محذوف&nbsp;&nbsp;&nbsp;&nbsp;Metre: </div> === ۵. حافظ — «در خرابات مغان نور خدا می‌بینم» === وزن: '''مضارع مثمّن اخرب مکفوف محذوف''' <pre> {{وزن بدایع سعدی|مفاعیلن|مفاعیلن|مفاعیلن|فعولن}} </pre> خروجی: <div dir="ltr" style="text-align:center; font-family:monospace; margin:0.5em 0;"> [⏑–––|⏑–––|⏑–––|⏑––]&nbsp;&nbsp;&nbsp;&nbsp;مضارع مثمّن اخرب مکفوف محذوف&nbsp;&nbsp;&nbsp;&nbsp;Metre: </div> === ۶. تعیین دستی نام وزن === اگر می‌خواهید نام وزن با آنچه در جدول است متفاوت باشد یا رکن‌ها در جدول نباشند: <pre> {{وزن بدایع سعدی|مفاعیلن|مفاعیلن|فعولن|وزن=هزج مسدّس محذوف}} </pre> یا با <code>بحر</code>: <pre> {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلن|بحر=رمل مسدّس محذوف}} </pre> == ردهٔ خودکار == ماژول در '''فضای نام اصلی (مقاله)''' به‌طور خودکار یک رده بر اساس ارکان می‌سازد. مثلاً: <pre> {{وزن بدایع سعدی|فاعلاتن|فاعلاتن|فاعلاتن|فاعلاتن}} </pre> صفحه را در <code>رده:فاعلاتن فاعلاتن فاعلاتن فاعلاتن</code> قرار می‌دهد. در '''فضای نام صفحه (Page:)''' و سایر فضاهای نام (الگو، رده، کاربر، …) رده اضافه نمی‌شود. '''نکتهٔ ProofreadPage:''' وقتی صفحه‌ای در فضای نام صفحه از طریق <code>&lt;pages&gt;</code> در یک مقاله تراگنجانده می‌شود، ماژول در بستر مقاله ارزیابی می‌شود، یعنی <code>mw.title.getCurrentTitle()</code> مقاله را برمی‌گرداند و رده به مقاله اضافه می‌شود — نه به صفحهٔ اثبات. این همان رفتار مطلوب است. == نکات فنی == * '''جهت متن:''' ظرف اصلی <code>direction: rtl</code> دارد تا ترتیب «نشانه‌گذاری → وزن → Metre:» از راست به چپ باشد. اما بخش نشانه‌گذاری <code>direction: ltr</code> دارد چون نویسه‌های <code>–</code>، <code>⏑</code>، <code>|</code>، <code>[</code>، <code>]</code> همه از ردهٔ bidi «خنثی» هستند و در بستر راست‌به‌چپ وارونه نمایش داده می‌شوند. * '''فاصلهٔ نویسه‌ای:''' <code>letter-spacing: 0.12em</code> روی هر الگو و <code>margin-right: -0.12em</code> متناظر با آن باعث می‌شود خط‌های پیوسته (مثل <code>––</code>) به‌صورت دو خط جدا دیده شوند، ولی میلهٔ <code>|</code> به الگو بچسبد. * '''شیوه‌نامه:''' تمام استایل‌ها در <code>Module:وزن بدایع سعدی/styles.css</code> قرار دارد و از طریق افزونهٔ TemplateStyles بارگذاری می‌شود. برای اعمال تغییرات ظاهری کافی است همان صفحه را ویرایش کنید. * '''بدون بازگردانی:''' ترتیب ارکان دقیقاً همان است که به الگو داده می‌شود. == گسترش جدول وزن‌ها == برای افزودن وزن تازه، یک سطر به جدول <code>meter_names</code> در ماژول بیفزایید: <pre> ["فاعلاتن فاعلاتن فاعلاتن فاعلان"] = "رمل مثمّن مقصور", </pre> کلید همیشه: رشتهٔ ارکان با فاصله، دقیقاً مطابق ترتیبی که به الگو داده می‌شود. '''یادآوری شَدّه:''' در نام وزن‌ها شَدّه روی حرف میانی باب تفعیل (مثمّن، مسدّس) و حرف آخر باب افتعال با ریشهٔ مضاعف (مجتثّ) می‌آید. سایر واژه‌ها (سالم، محذوف، مخبون، مکفوف، اخرب، و نام بحرها) شَدّه ندارند. <includeonly>[[رده:الگوهای وزن عروضی]]</includeonly> <noinclude>[[رده:صفحه‌های توضیحات الگو]]</noinclude> 2fbhn2ilezlzo6ljwtllqutwwg7la0r الگو:شعر۲ 10 97141 304010 2026-10-11T20:28:17Z Hanooz 17889 صفحه‌ای تازه حاوی «<includeonly>{{#invoke:شعر|open}}</includeonly><noinclude>{{توضیحات}}[[رده:الگوهای شعر]]</noinclude>» ایجاد کرد 304010 wikitext text/x-wiki <includeonly>{{#invoke:شعر|open}}</includeonly><noinclude>{{توضیحات}}[[رده:الگوهای شعر]]</noinclude> 9vl5o1b9he7bu1qhewafzbe5oimjak7 بحث:دیوان حافظ/سحرم دولت بیدار به بالین آمد 1 97142 304017 2026-10-11T20:35:28Z ~2026-54745-61 40107 /* هویت ایرانی */ بخش جدید 304017 wikitext text/x-wiki == هویت ایرانی == استادان گرامی، از بکار گیری واژه های انگلیسی و فرانسوی خو داری کنید. واژه ای برای استاندار پیدا کنید. واژهایی در ردیف «معمول» ویا «رایج» میتواند گویای منظور باشد. چندش آور بود که روزی از زبان سخنگوی آموزش و پرورش واژه « اکسپرت» را شنیدم ویا دهقانی که اژه «تایم» را به زبان آورد. این پدیده مثل بیماری واگیری به زبان ایرانیان افتاده. فرهنگ ایرانی بر پایه زبان فارسی استوارست. تیشه به ریشه خود نزنیم! [[ویژه:مشارکت‌ها/~2026-54745-61|~2026-54745-61]] ([[بحث کاربر:~2026-54745-61|بحث]]) ۱۱ اکتبر ۲۰۲۶، ساعت ۲۰:۳۵ (UTC) a5jbijv9i4ssz43u35fhp4xkf3iffdc بدایع سعدی/شعر شماره ۵ 0 97143 304018 2026-10-11T20:43:37Z Hanooz 17889 صفحه‌ای تازه حاوی «{{header | title = [[../]] | author = سعدی | editor = تقی ارانی | translator = | section = ۵ | previous = [[../شعر شماره ۴|۴]] | next = [[../شعر شماره ۶|۶]] | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=20 to=21 fromsection="p20-21...» ایجاد کرد 304018 wikitext text/x-wiki {{header | title = [[../]] | author = سعدی | editor = تقی ارانی | translator = | section = ۵ | previous = [[../شعر شماره ۴|۴]] | next = [[../شعر شماره ۶|۶]] | notes = }} <pages index="بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf" from=20 to=21 fromsection="p20-21" tosection="p20-21"></pages> {{خطکش}} {{پانویس|چپ‌چین=بله|اندازه=ریز}} 03jenfeypd1z0q632gia4pd3x8c4tea برگه:بدایع غزلیات افصح المتکلمین شیخ مصلح‌الدین سعدی شیرازی - چاپخانه شرکت کاویانی.pdf/۲۱ 104 97144 304019 2026-10-11T20:49:03Z Hanooz 17889 /* نمونه‌خوانی‌نشده */ صفحه‌ای تازه حاوی «{{شعر۲|متن= سعدی ز فراق تو نه آن رنج کشیده است کز شادی وصل تو فرامش کند آنرا گر تیر جراحت زند و <ref name=p7-n8>I(a); مگیر, for مده.</ref> باز هم آید از جای جراحت نتوان برد نشانرا<ref name=p7-n8/> }} <section end="p20-21" />» ایجاد کرد 304019 proofread-page text/x-wiki <noinclude><pagequality level="1" user="Hanooz" />{{سرصفحه‌مستمر||بدایع شیخ سعدی|num}}</noinclude>{{شعر۲|متن= سعدی ز فراق تو نه آن رنج کشیده است کز شادی وصل تو فرامش کند آنرا گر تیر جراحت زند و <ref name=p7-n8>I(a); مگیر, for مده.</ref> باز هم آید از جای جراحت نتوان برد نشانرا<ref name=p7-n8/> }} <section end="p20-21" /><noinclude>{{خطکش}} {{پانویس|چپ‌چین=بله}}</noinclude> kci1m6x73dcp5nniyg22xzxmjui5d33