感叹自己傻的句子 形容往后余生相守的句子
你现在阅读的是一篇关于感叹自己傻的句子的文章,里面有丰富多彩的内容,还有给你准备形容往后余生相守的句子和感叹自己傻的句子的精彩内容哦。
感叹自己傻的句子 形容往后余生相守的句子
一生,百年时间,
看似漫长,实则沧海一粟;
一生,从小到老,
看似遥远,实则匆匆之间。
一生,
忙忙碌碌间,就几十年,
时光飞速过,毫不留情。
我们已经很累,
千万别让自己的心更累。
这一生太短太难,
请你多取悦自己,
做一个快乐的傻子!
01、人啊,别胡思乱想
人不多思,便不累;
心不多想,便不烦。
心事,本能大事化小小事化了,
想得多了就成了事。
很多心病,
都是自己胡思乱想想出来的。
人生得失一辈子,
我们能左右的东西不多,
学会看淡,顺其自然,
才能够快乐!
02、得与失,天安排
得到和失去,
这一辈子多长多远,
都是上天注定好的。
愁苦个什么劲?
是吃不上饭?还是没地方睡觉?
自己有窝,身边有爱,
便是顶好的生活。
我们要学会珍惜,
珍惜每一天的岁月静好;
我们要学会放下,
放下心里面的糟粕烦恼。
余生,让自己过得尽量轻松,
顺其自然活着,就是最好!
03、人生无常,心安即是归处
人和人之间的缘分,
到底能处多久?
有些人强求还是会走,
有些人不求一直停留。
对的人,才会携手;
错的人,早晚殊途。
恩恩怨怨别放心里,
糟粕烦恼留在过去。
做好自己,一切听从天意,
咱这一辈子,
心安就是最好的状态!
人生,不如意事十之八九,
常想一二,不思八九!
路再荆棘,也会变得平坦;
事再曲折,也早晚会过去。
静下心来沉淀自己,
笑对生活取悦自己。
让自己开心的活,
烦恼便离你远去;
让自己快乐的过,
这一生才最值得!
描写奶奶外貌的句子(被91岁的神仙奶奶惊艳到了)
一个女人看起来是否年轻,其实无关乎年龄,而在于心态和状态。有的人年龄并不是很大,看起来却是一脸暮气,缺少女性该有的光彩与魅力。相反,也有一些上了年纪的女性,凭借出彩的仪态与神韵,一眼看上去便能被她的魅力所折服。
因为忙着美丽,所以没时间变老,这是她的生活状态,爱美的女人就是这么自信。
有魅力的女人从来不会被世俗所裹挟,她们对生活充满了期待与激情,这也让自己的人生赢得了更多的可能性。
这位奶奶的身材并不是很高挑,容貌也不是很出众,但是,对美好生活的渴望,让她拥有着一颗永远年轻的心。在她70多岁的时候,成功进入了模特行业,90岁的时候又开始直播带货,而且收获粉丝无数,除了成熟女性欣赏她,也很受年轻人的欢迎。
她喜欢一切美好的东西,在服装颜色的选择上从不受限。很多老人不敢轻易尝试亮色调的服装,各种担心,各种纠结,使得她们只愿意选择安全的暗色调单品。虽然不会出错,但是也缺少了几分趣味性。
亮色吸睛又显气色
这位奶奶大胆选择亮橙色的旗袍,鲜艳的色彩烘托出快乐又甜蜜的氛围,同时又将古典雅致的韵味进行书写,一款花色的披肩搭在肩上,既能修饰身材又能营造出优雅的女人味。
一头银色的短发梳理得一丝不苟,清爽干净的妆面衬托出健康的气色,优雅从容又很显气质。鲜艳的绿色旗袍是非常清爽又吸睛的颜色,在人群中总是能够一眼被发现,想要穿搭好看,也需要各方面的配合。
淡妆增添气色
首先是肤色的调整极为重要,肤色白皙的女性更加适合选择这种亮眼的色调,若是对自己的肤色不够自信,则需要运用粉底来进行修饰,将肤色修饰的均匀透亮,会为整体的美感增色不少。淡淡的妆容可以立刻调整气色,也会大大降低穿搭难度。
一款合适的发型,会为自己的形象增色不少,有人说换个发型换张脸,虽然有些夸张,但是也说明的发型对于一个人的形象起到的作用是不容小觑的。
短发干练又显气质
随着年龄的增长,皮肤逐渐松弛,这时候,造型线条向上的感觉会显得更加精神,又能起到减龄的效果。一款蓬松的短发是老年人最适合的发型之一,除了很好打理以外,也会因为蓬松的造型,显出发量的充沛,同时起到提升气质的作用。
都知道白发是显老的标致,自从有了第一根白发,便不能阻挡白发越来越多的这种现实状况,但是,只要选择合适的发型,白发并不会带来颓废的感觉,反而能够增添气场,彰显气质。
像这位奶奶选择的这款发型,蓬松的短发全部梳向脑后,露出全部的五官,看起来清爽又干练。饱满的外形,形成头包脸的画面,不仅显得脸型更加秀气,而且也会有着整体向上的感觉,更加显得神采奕奕。
很多女性流连于旗袍的端庄优雅,古典的韵味透露出女性的温柔。这位奶奶穿上旗袍优雅迷人,穿上便装的她则显得干练洒脱,手拿专业相机的她,满满的艺术范儿。老有所为,老有所爱,以自己喜爱的方式度过每一天,才是最充实的。
裤装洒脱又有活力
红色上衣搭配白色的长裤,既清爽又有活力,内搭同样选择白色的单品,上下颜色形成色彩的顺延,而内长外短的搭配方式又能划分出丰富的层次感,顺便打造出好看的身材比例。胸前搭配一款白色的珍珠项链,既能填充空白,又能彰显出女性的优雅韵味。
一身同色的设计是永远不会过时的装扮,有一种化繁为简的审美态度,尤其是一身白色的造型,更加体现出简约又高级的画面。
白色西装采用修身的设计,线条简洁流畅,恰到好处的收腰设计显出身材的曲线,下半身搭配一条白色的西装裤,笔直的中缝线条带来正式的感觉,同时又能显出腿型的修长。一款印花丝巾系在胸前作为点缀,既保暖又能化解白色的单调。
俗话说,千金难买老来瘦,保持一个苗条的身材,既能带来身体的健康,也能塑造出优美的身姿。这位奶奶曾经也胖过,导致身体健康出了问题,随着她不间断的锻炼,身体越来越健康,身材也越来越好。
优雅的体态需要苗条的身姿,身材好穿衣服自然好看。锻炼中的奶奶身穿修身粉红色运动衣加黑色运动裤,脚踩粉红色运动鞋,看起来活力四射又自信满满,一点不没有扮嫩的尴尬,露出甜蜜的微笑,清切又自然。
本文由成铭聊时尚原创,仅代表个人的观点,图片均来自网络,如果有侵权请联系删除。希望我专业的解读和独特的见解能帮到大家,更多话题可在下面评论区继续交流或吐槽。
早上好的句子短语(2023)
早上好,今天是2023年01月07日,星期五,十二月初五,辛丑年 【牛年】 辛丑月 庚申日!
每天告诉自己要努力,即使看不到希望,也依然相信自己,压力不是有人比你努力,而是比你牛叉几倍的人依然在努力,每个优秀的人,都有一段沉默的时光,那段时光,是付出了很多努力,忍受孤独和寂寞,不抱怨不诉苦,日后说起时,连自己都能被感动的日子,唯累过,方得闲,唯苦过,方知甜。
一点一点去靠近梦想,不抱侥幸的奢望,用踏实浇灌,用努力证明,你可以,早安!
生命中遇到的问题,都是为你量身定做的,你要勇敢面对,有些路,没有捷径;有些事,没有绝对;有些人,没有懦弱,只有坚强,要知道,人生的弯路,非走不可,早安!
所有的困难,实际都是好事,它在推着你往前走,让你更强大也更有力量,早安!
懒惰不会让你一下子一无所有,但会在不知不觉中减少你的收获!勤奋也不会让你一夜成功,但会在不知不觉中积累你的成!人生需要挑战,更需要坚持和勤奋,早安!
当成熟代替了幼稚,当脆弱败给了坚强,我们一定会走到自己曾经憧憬过的地方,新的一天,早安!
不看表面看实质,或许你的努力暂时看起来没那么大的回报,但是隐藏在后面的却是每天茁壮成长,等到收获的那天却是大大的回报,早安!
都说女孩子不要太要强、太独立、太厉害,不然会不招人喜欢可是,我若不要强、不独立、不变厉害,谁又会在我最无助的时候伸出援手?所以我只能让自己坚强!早安!
女孩子,拥有独立的人格,懂得照顾好自己,在事情处理妥帖后能尽情享受生活,不常倾述,自己的苦难自己有能力消释,不因小事随便发脾气久久不能释怀,内心强大而能生出一种体恤式的温柔,不被廉价的言论和情感煽动,坚持自己的判断不后悔,愿你成为这样的人,一个最棒的女人,早安!
风可以吹起一张白纸,却无法吹走一只蝴蝶,因为生命的力量在于不顺从!
把每—次的失败都归结为一次尝试,不去自卑;把每一次的成功都想象成一种幸运,不去自傲。就这样,微笑着弹奏从容的弦乐,去面对挫折,去接受幸福,去品味孤独,去战胜忧伤。早安。
再色彩鲜艳的画面也会褪色,再爱憎分明的情感也会模糊,再无所畏惧的勇气也会冷却。所以,趁画还鲜明,心还未累,勇气尚满;出发吧,挥霍青春,张扬年少,追逐心之所愿,别负风华正茂。早安!
两个人相处久了,难免会抱怨一句你变了,也许我们并没有改变,我们只是越来越接近真实的对方。早晨好!距离之所以可怕,因为根本不知道对方是把你想念,还是把你忘记;有时候我只是需要听到你说:一切都会好的。星期五,早安,朋友!
最好的地方,是没去过的地方;
最好的时光,是回不来的时光。
趁年轻,多经历,多奋斗!
每天都用一个崭新的自己,迎接美好的未来。
每个人的生活里都会有遗憾,没有遗憾,就不懂得美好的意义,放下怨恨执念,每一天要告诉自己,今天是美好的,积极、阳光、感恩、知足、快乐。
努力从来不会白费,今日撒下种子,正在你看不见、想不到的某处,悄悄地生根发芽,你不努力谁也给不了你想要的生活,越努力,越幸运,早安!
时间,抓起了就是黄金,虚度了就是流水;书,看了就是知识,没看就是废纸;理想,努力了才叫梦想,放弃了那只是妄想。努力,虽然未必会收获,但放弃,就一定一无所获。再好的机会,也要靠人把握,而努力至关重要。放手去做、执着坚持!
生活就像一只蝴蝶,如果没有破茧的勇气,就不会有飞舞的美丽!生活又像一只蜜蜂,如果没有勤劳和努力,就不会尝到花粉的甜蜜!所以越努力越幸运!早安!
每个人的心中都有一匹欲望的野马。你可以去放养它,也可以去圈养它驯养它。但是不要假装它不存在,排斥它。我们每个人都应该自由的,骄傲地活出个生机勃勃的生命。早安!
要时常告诉自己,虽然在最低的位置,看不到花朵绽放时的艳丽,然而却不会错过花瓣飘落时在风中悠扬飞舞的浪漫。记住,可以哭,可以恨,但是不可以不坚强。因为后面还有一群人在等着看你的笑话。
没有人天生是工作狂,只因为不想落人后、只因为不想输得难看,于是就努力努力工作,一不留神,把人生过得很带劲!
不是每个人都能成为,自己想要的样子,但每个人,都可以努力,成为自己想要的样子。相信自己,你能作茧自缚,就能破茧成蝶。早安!
人生的不平,正如这雨中的泥泞,需要努力才能跨过,生活中的美丽,就如这雨中的润泽,需要静心去品尝,那么,有拼搏、有感悟的人生,才会萌发更多的美好和向往,早安!
岁月匆匆,时光悠悠,经不住似水流年,也留不过岁月变迁,转眼间,十月即将接近尾声…愿自己:在川流不息的时光里,依旧神采飞扬,活出精彩,早安!
任何一个人,成就一番事业:难在看懂,停在情绪,慢在依赖,快在承担,赢在跟对,乱在不定,苦在单干,巧在借力,亏在自私,散在随意,错在指责,胜在反省,累在盲目,贵在付出,输在少学,败在放弃,成在坚持。早安
你做不出来的题,总有人能做出来;你学了一下就放弃的技能,总有人会做到专精;你拖拉想着明天再做的事,总有人在今天做完;你想了几百遍却从未实施的计划,总有人在认真耕耘。不要假装很努力,因为结果不会陪你做戏。
做事,不要总是瞻前顾后。有些事纯属机缘,耽误了就没有下一次。有些人过去了,或许再也见不到了。只有迈出脚下那一步,人生才会与众不同。机不可失,时不再来,畏首畏尾,左顾右盼,机会就失去了。
不要再轻易被别人影响情绪啦,多关注天气和身体,把自己裹在暖和的衣服里,按照自己的节奏努力,开心或者不开心的时刻都请多爱自己。
一定要跟越来越美的人做朋友,因为美貌的背后 ,藏着一个人对自己的自律、坚持、克制和对高品质的追求。
今天你的每一个看似平凡的努力,都是在为你的未来积累能量;今天你所经历的一切,都是在为未来打基础!所以,昨天怎么样不重要,关键是今天做了什么,明天怎么样!早安!
年轻的时候,不让自己去历经繁华,你就亏欠了自己。年纪大了不让自己恢复到简单,你就可能亏欠生命。人生从纯粹、天真,走向虚伪、复杂,再到返朴归真,人人都走在这条路上,没有比这更像命运的命运。
人生从来没有固定的路线,决定你能够走多远的,并不是年龄,而是你的努力程度。无论到了什么时候,只要你还有心情对着糟糕的生活挥拳宣战,都不算太晚。迟做,总比不做好!
每次被人排挤的时候,你总会以为自己错了,每次被人谩骂的时候,你因此郁闷了几天,生活中少不了一些无缘无故看不起你的人,也会碰到一些无缘无故讨厌你的人,不必因此而影响自己的心情。在你回家的路上,总会遇到几条疯狗在路边乱吠,你只需继续前进,无须理会。生活就是一边受伤,一边学会坚强。
面对生活,
每个人都过得不容易。
太阳升起,
就要拼了力气的去争取;
深夜无眠,
还要想着明天怎么继续。
肩上的压力,
是生存的难题;
心中的压抑,
又是不能说的委屈。
生活不容易,要多爱自己,
谁都有负重,别去攀比。
人这一辈子,
干干净净的活着,
享受人生不沉溺,
经历苦涩不消极。
善良的你,终有好报,
坚持的你,必有收获。
珍惜当前拥有,
努力活好当下!
当你越来越有魅力的时候,自然有人关注你,当你越来越有能力的时候,自然会有人看的起你,改变自己,你才有自信,梦想才会慢慢的实现
台上三分钟,台下十年功。”哪个成功人的背后不包含汗水和泪水?记住了,没有谁可以随随便便,轻轻松松就能成功。
什么是运气?准备+等待=运气。我们常常在羡慕别人好运时,却不知时时为自己做好准备。人生没有捷径,所谓的捷径,只不过是耐心而已。如果你没一个好的耐心,那么你就永远没有机会,如果你没有机会,那你永远就不会成功。
记住,耐心就是机会,机会才会成功!
人生一辈子,写自己让别人读,你不让都不行;读别人对照自己,用放大镜看别人的真、善、美,吸取营养,完善自我;用老花镜看别人的假、恶、丑,模糊掉、虚化掉。写自己要认认真真。因为在人生的写作里是没有“修改”和“删除”;读别人,不妨由表及里、去伪存真!
"愿你在换季的衣服里发现花剩的零钱,愿你在下雨天时路上的空车都不拒载,愿你尝试新的食物时都比想象中要更好吃,愿你做过的美梦都不要忘记,愿你的心情永远都像星期五的午后一样轻松自在,愿你爱的人永远爱你,愿你永远活得像个孩子,愿路途遥远都有人陪在你的身边。"
余额不足可以挣,电量不足可以充;时间走了就不再回来,你付出多少,就会收获多少,不攀比,不抱怨,不计较,多付出,因为有一种努力叫———靠自己!
最好的地方,是没去过的地方;最好的时光,是回不来的时光。趁年轻,多经历,多奋斗!每天都用一个崭新的自己,迎接美好的未来。
既然怀揣一颗雄心,就别在白眼中呻吟。站起身来,为自己呐喊。把风雨撇入脑后,把憧憬揣进胸膛。别人说你不行,自己说自己能行,那么我们依旧是能前行。苦涩的泪水,每一滴都折射出生命的伟岸和情感的纯真。生无所息,每一个人都该追求梦想和希望。我们需要心灵磨练,梦想去成长。
要想往高处飞,就别在原地打转, 要想向远方走,就别被困难吓退。时间是公平的,你把它用在哪里, 哪里就会开花结果,真正努力的人都会越来越强大!
别一身都是名牌,却有一张被生活欺负的脸!赚钱当然重要,但如果这钱赚得苦大仇深,把它全堆在身上也装点不出你的美。人生的成功,在于如何在成功和快乐间寻求平衡。——希望你衣食无忧。希望你有一张不被生活欺负的脸。
努力就有希望,前进就有希望,活着就有希望——眼下虽有寒冷,过后总有暖阳!
人的生命只有一次,但生活可以每一天都不同。面对快速变化的世界,我们就要把过去放下、把现在扛起,把每一天当成一个新的开始,牢记心中的理想,微笑面对生活,不断充实自己,就会拥有精彩有意义的人生。
人生要走的路,没有一条是容易的路。我们只能选择一条更适合自己的路,然后凭借自己的努力,去决定自己的样子,去过上最想要拥有的那一种生活。朋友们早上好!
没人愿意去扶持一个毫无价值的人,
所以你要经营好自己,
就算跌入谷底也要有与人交换的筹码,
千万别让自己一文不值 。
看着温暖的文字,心就会变得坚强,就好像这世界到处阳光明媚一样,充满了正能量。不念过去,不畏将来,我想我也会做得很好。早安!
新的一年,我们都要好好生活。做个知足人,别无他求,安稳自由;做个重情人,不谈亏欠,不负遇见;做个乐观人,不卑不亢,随遇而安,做个简单人,没心没肺,过好每一天。
保持阳光心态,积极面对人生。每个人,都沿着不同的轨道在活着,人生是一趟单程车,我们最应该做的,就是好好善待自己,珍惜今天,期待明天。早安
新的一天,不必纠结,放下过去,拥抱生活;不必慌张,活好当下,来日方长;不必失望,人间值得,未来可期。
一个人只有挺住今天含泪的耕耘,才会赢得明天欢笑的收割;人不要急于等着回报,只要你种下种子,就一定会有收获!只管耕耘,不问收获,因为播种和收获往往不在同一个季节!有时候,不是树太高,而是你没有努力往上爬,不是井里没水,而是挖的不够深,不是成功来的慢,是放弃的快;成功不是靠奇迹,而是靠耕耘到成熟的轨迹!
行动力,是我们对平庸生活最好的回击。人与人之所以拉开距离,就在于行动力。不行动,梦想就只是好高骛远;不执行,目标就只是海市蜃楼。想做一件事,就请马上开始!
人生是一本书,封面是父母给的,内容是自己写的,厚度由本人决定,精彩程度可以自己创造。不是每一次努力都会有收获,但是,每一次收获都必须努力,这是一个不公平的不可逆转的命题。星期五,早上安康,我的朋友!
如果你不相信努力和时光,那么成果就会是第一个选择辜负你的,不要去否定你自己的过去,也不要用你的过去牵扯你现在的努力和对未来的展望,不是因为拥有希望你才去努力,而是去努力了,你才有可能看到希望的光芒,早安!
每一个被幸运垂青和眷顾的人,背后都藏着无数努力和汗水。翻山越岭,长途跋涉,等待机遇,大展身手。谁又能平白无故地成功,每一步都艰辛苦涩,所以不必比较和羡慕,走好脚下的路,专注在自己身上,耐心地经营,那些小惊喜迟早会变成一扇窗,让你眺望更美好的远方。
第一山今日头条官方账号早安心语正能量励志分享图文,每天分享早安心语、早安心语正能量、早安正能量、早安心语励志、正能量语录、早安图片等句子说说,用正能量点亮您幸福美好的一天!
推荐阅读:
①「2023.01.06」早安心语,正能量暖心文案句子冬天早上好图片最新
②「2023.01.05」早安心语,小寒正能量新潮语录句子,唯美带字图片
③「2023.01.04」早安心语,正能量新潮语录句子,2023早安励志图片
④「2023.01.03」早安心语,正能量优美语句,2023最新版早上好图片
⑤「2023.01.02」早安心语,周末你好!前路浩浩荡荡,万事尽可期待
??????.????7?
﹊﹊﹊﹊﹊﹊﹊
简单一点过,灿烂一点活,乐观向上,敢于担当。人生,无法尽善尽美。纵然时光薄凉,内心依旧向暖。与阳光相依,微笑前行。愿我们活在当下,珍惜拥有;余生安好,未来可期。
??Good Morning……
描写奶奶外貌的句子(写实风格的火影人物)
火影的人物风格
说起火影的设定,本应该是一种近代的风格,毕竟涉及日本武士和忍者的时代,但是看动漫的话,又感觉里面有很多现代化的东西(比如对讲机)。虽然我们知道火影的背景设定是一个架空世界,所以将这两者结合起来没有什么问题。
但是近代和现代的风格显然是不一样的,尤其是在穿着和忍者相关的服装之时,又该用什么的方式去设计人物呢,所以我们看到的火影忍者其实是一个混搭风格的人物画风,让人捉摸不透到底应该是什么类型的风格。
或许有人觉得这种混搭的风格没有什么问题,但是却忽视了一个问题,那就是这种风格可能会影响人们对于颜值的判断。比如小樱和井野明明设定上是属于颜值相当的水准,但是明显在第一部里面她们都比不过红、红豆、纲手等等。而到了第二部,井野的造型一变,立马就符合她美女的身份设定了,而小樱依旧还是那个样子。
这种情况一直到《博人传》才有所转变,小樱终于符合了她女主的颜值,同时也是靠着几位老牌美女颜值衰退过多所形成的一种对比反差。如果单纯以为是造型问题,可能还没有抓到重点,其实很主要的原因就在于动画风格了。
很明显小樱原本的造型不适合偏向于近代的火影风格,而到了《博人传》之后,由于一开始抬高了科技的地位,使得整体风格向现代靠拢,于是小樱的造型随着风格的变化而改变,她也终于回归了自己的美女本质。
火影采用写实风格
火影原本的风格不仅影响了女主小樱的颜值判断,也影响了对两个男主的颜值判断。尤其是到了后期,鸣人颜值提升,佐助颜值下降,甚至给人一种错觉好像二人的颜值其实没差多少,这就误会大了。
在二人尚在忍校之时,佐助是属于校草级别的,鸣人虽然不至于像丁次那样垫底,可也只是普普通通的类型。
这正是岸本想要给表达的地方,就像《千与千寻》中相貌平平的千寻一样,鸣人如果不考虑“外挂”的话,就是一普通长相的人,他最后之所以能够达到如此境界,很大程度上还是取决于他自身的努力。
所以这个风格上的影响真的是让鸣人被误会大了。我们知道,像佐助这种类型的在学校即使就是坐在桌子上不说话,也是一个“安静的美男子”,而鸣人必须要做出很大的成绩,比如在篮球场上击败了全校最强的队伍,又或是外校来“踢馆”打败了校队,却输给了鸣人。只有发生这样的事情,才能够让人觉得鸣人很帅气。
其实仙鸣的样子一点儿也不好看,之所以那么帅气,全拜他的实力所赐,这也是为什么他在击败佩恩回村后才被井野等人开始觉得鸣人很帅气的原因了。
如果按照写实的风格来便会发现,鸣人远没有他老爸水门颜值高,但是和宇智波一家的带土、佐助比起来,又不在一个层次。而女角色这边,纲手的颜值是真的“耐打”,无论怎么换风格都是一样高。
句子成分分析器(字节工程师自研基于)
前言
众所周知,程序员最讨厌的四件事:写注释、写文档、别人不写注释、别人不写文档。因此,想办法降低文档的编写和维护成本是很有必要的。当前写技术文档的模式如图:
痛点总结有如下三方面:
针对上述问题,我们的解决思路:
- 本地的编辑、浏览工作收敛至 IDE,提供沉浸式体验;
- 在文档、代码间建立强关联,减少拷贝,提升联动性,同时提升文档的触达率;
- 代码与文档同属一个 Git 仓库,借助版本管理,避免因业务迭代导致的文档版本与代码不匹配;
- 制作可将文档导出到线上的工具,可利用浏览器做到随时访问;
方案总览
与原始模式相比,新方案可以做到完全脱离浏览器 / 文档编辑器,线上页面的同步完全交给定时触发的自动化部署。
图中橙色部分是方案的重点,按照分工,划分为线下、线上两部分,职责如下:
- 线下:IDEA Plugin
- 实现自定义语言的解析、分析;
- 提供文档内容的预览器、编辑器;
- 提供一系列实用功能,关联代码与文档;
- 线上:Gradle / Dokka Plugin
- 桥接、复用 IDE Plugin 的语义分析、预览内容生成能力;
- 扩展 Dokka Renderer,实现 HTML 与飞书文档的导出能力;
方案建设使用了不少有意思的技术,放到后面详细介绍。
线下效果
IDEA Plugin 提供一个侧边栏和强大的编辑器。下面分别从编辑、浏览两个角度介绍。
编辑体验
假设存在源码如下:
public class ClassA { public static final String TAG = "tag"; ClassB b; /** * method document here. * * @param params input string */ public static void invoke(@NotNull String params) { System.out.println("invoke method!"); System.out.println("this is method body: " + params); } public ClassA() { System.out.println("create new instance!"); } private static final class ChildClass { /** * This is a method from inner class. */ void innerInvoke() { System.out.println("invoke method from child!"); } }}
文档中添加该类的引用就是这个效果:
不同于复制、粘贴代码,新方案有如下优势:
- 关联性更强,预览会随代码片段的变更时时改变;
- 易于重构,被引用的类名、方法名、字段名发生重命名时,文档内容会自动随之变化,防止引用失效;
- 更加直观,编辑、浏览时能更快速地找到代码出处;
- 输入更流畅,有完善的补全能力;
浏览体验
相对于普通 Markdown,新方案用起来更加友善:
- 沉浸式使用,界面内嵌在 IDE 内,无需跳转到其他应用;
- 被提及的源码旁均有行标,点击一键查阅文档;
- 文档“浏览器”支持与 IDE 一致的代码高亮、引用跳转;
线上效果
代码中文档会定期自动部署到远端。以一篇真实业务文档举例,HTML 部署到轻服务后长这样:
对应飞书的产物长这样:
这些线上页面主要面向非当前团队的读者,内容由 CI 定时同步,暂不提供跳转到 IDE 的能力。
技术实现
项目的架构如图所示:
考虑到用户体验部分主要在 IDEA(Android Studio)内呈现,我们的技术栈选择基于 IntelliJ 打造。按模块可分为三部分:
- 基建层
- IDEA Plugin
- Gradle / Dokka Plugin
通用逻辑(语言实现相关)封装在基建层,仅依赖 IntelliJ Core。相对于 IntelliJ Platform,IntelliJ Core 仅保留语言相关的能力,精简了 codeInsight、UI 组件等代码,被广泛用于 IntelliJ 各大产品中(包括图中的 Kotlin、Dokka 等)。
下面将针对这三个主要模块展开介绍。
基建
纵观整个方案,基建层是所有功能的基石,其最核心的能力是建立代码与文档关联。这里我们设计实现了一套标记语言 CodeRef,满足以下几个需求:
- 语法简洁,结构上与源码一一对应;
- 指向精准,即必须满足一对一的关系;
- 支持仅保留声明(去掉 body),提升信噪比;
- 有扩展性,方便后续迭代新功能;
CodeRef 语言并不复杂,采用类似 Kotlin/Java 的风格,用关键字、字符串、括号构成语句和代码块,代码块中每个节点都有与之对应的源码节点。下图是一个简单的示例,对应关系用着色文字标识:
注意:即使不改动文档内容,图中“源码”部分一旦发生变化,对应的渲染效果也会实时发生改变,产生“动态绑定”的效果。那么如何实现“动态绑定”呢?大致拆解成以下三步:
- 设计语法,编写语言实现;
- 结合现有能力(IntelliJ Core、Kotlin Plugin)获取双边语法树,从而建立文档节点到源码节点的单向对应关系;
- 结合现有能力(Markdown Parser)生成用于渲染的文档文本;
语言基础实现
基于 IntelliJ Platform,实现一个自定义语言起码要做以下几件事:
- 编写 BNF 定义,描述语法;
- 借助 Grammar Kit 生成 Parser、PsiElement 接口、flex 定义等;
- 基于生成的 flex 文件和 JFlex 生成 Lexer;
- 编写 Mixin 类用 PsiTreeUtil 等工具实现 PSI 中声明的自定义方法;
BNF 是后面一切的基础,每个定义、值的选择都至关重要。一小段示例:
{ /* ...一些必要的 Context */ tokens = [ /* ...一些 Token,转换为代码中的 IElementType */ AT='@' CLASS='class' ] /* ...一些规则 */ extends("class_ref_block|direct_ref|empty_ref") = ref extends("package_location|class_location") = ref_location extends("class_ref|method_ref|field_ref") = direct_ref}ref_location ::= package_location | class_locationpackage_location ::= AT package_def { pin=2 // 只有 '@' 和 package_def 一起出现时,才把整个 element 视为 package_location}class_location ::= AT class_def { pin=2 // 只有 '@' 和 class_def 一起出现时,才把整个 element 视为 class_location}direct_ref ::= class_ref | method_ref | field_ref | empty_ref { methods = [ // 一些自定义的 method,需要在下面指定的 mixin class 中给出实现 getNameStringLiteral getReferencedElement getOptionalArgs ] mixin="com.bytedance.lang.codeRef.psi.impl.CodeRefDirectRefMixin"}class_ref ::= CLASS L_PAREN string_literal [COMMA ref_args_element*] R_PAREN { methods = [ property_value="" ] pin=1 // 即遇到第一个元素 class 后,就将当前 element 匹配为 class_ref}
上面的小片段中定义了 @class("")、@package("")、class("", ...) 语法。实战中比较关键的是 pin 和 recoverWhile,前者影响一段“未完成”的代码的类型,后者控制一段规则何时结束。具体参考 Grammar-Kit。
编写完成后,我们就可以使用 Grammar-Kit 生成 Parser 和 Lexer 了,前者负责最基础的语法高亮,后者负责输出 PSI 树。将二者注册在自定义的 ParserDefinition,再结合自定义的 LanguageFileType,相应类型文件就会被 IDE 解析成由 PsiElement 构成的树。示意如图:
值得一提的是,后续 Formatter、CompletionContributor 等组件的实现受上述过程影响极大,实现不好必然面临返工。而偏偏这里面又有不少“坑”需要一一淌过,这部分限于篇幅没办法写得太细,有兴趣看看语言特性“相对简单”的 Fortran 的 BNF 定义感受一下。
语法树单向对应
考虑到 IDE 内置了对 Java、Kotlin 语言的支持,有了上一步的成果,我们就得到了两颗语法树,是时候把两棵树的节点关联起来了:
这里我们借用 PsiReferenceContributor(官方文档) 注册 CrElement(即 CodeRef 语言 PsiElement 的基类)向源码 PsiElement 的引用,依据便是每行双引号内的内容(字符串)。如何找到每个字符串对应的元素呢?遵循以下三步:
- 除根节点外,每个节点需要向上递归找到每一级 parent 直至根节点;
- 根节点是给定 full-qualified-name 的 package 或 class,由上一步的结果可确定元素在该 package 或 class 中的位置;
- 通过 JavaPsiFacade 和一系列查找方法确定源码中对应的 PsiElement;注意:Kotlin Plugin 提供一套针对 Java 的 “Light” PsiElement 实现,因此这里我们考虑 Java 即可。
生成文档文本
有了语法树对应关系,就可以生成用于预览的文本了。这部分比较常规,时刻注意读写环境,按照以下步骤实现即可:
- 为每个 CodeRef 语法树根节点指向的源码文件创建副本;
- 遍历该 CodeRef 树中每个 Ref 或 Location,创建或定位副本中对应位置,将源码文件中的元素(修饰后)复制到副本中;
- 导出副本字符串;考虑到 IDE 中 PSI 和文件是实时映射的,为不影响原文件内容,必须在副本环境中进行语法树的增删改。
这部分虽然难度不大,繁琐程度却是最高的。一方面,由于要深入到细节,使得前文提到的 Kotlin Light PSI 不再适用,因此必须针对 Java 和 Kotlin 分别编写实现。另一方面,如何保证复制后的代码格式仍是正确的也是个大问题,尤其是涉及元素之间穿插注释的情况。最后,文本内容生成的工作在不停的断点、调试的循环中玄学般地完成了。
至此,基建层的任务——将 CodeRef 还原成代码段——便全部完成了。
IDEA Plugin
有了前面的基础,IDEA Plugin 主要负责把方案的本地使用体验做到可用、易用。具体来说,插件的功能分为两类:
- 面向 CodeRef,丰富语言功能;
- 面向 Markdown,提升编辑、阅读体验;
接下来分别从以上角度介绍。
语言优化
对于一门“新语言”,从体验层面来看,PSI 的完成只是第一步,自动补全、关键字高亮、格式化等功能对可用性的影响也是决定性的。尤其是在 CodeRef 的语法下,指望用户能不依赖提示手动输入正确的包名、类名、方法名,无疑过于硬核了。下面挑几个有意思的展开说说。
代码补全
在 IDEA 中,大部分(不太复杂的)代码补全使用 Pattern 模式注册。所谓 Pattern 相当于一个 Filter,在当前光标位置满足该 Pattern 时就会触发对应的 CompletionContributor。
我们可以使用 PlatformPatterns 的若干内置方法描述一个 Pattern。比如一段 CodeRef 代码:method("helloWorld"),其 PSI 树长这样子:
- CrMethodRef // text: method("helloWorld") - CrStringLiteral // text: "helloWorld" - LeafPsiElement // text: helloWorld
Pattern 因此为:
val pattern = PlatformPatterns.psiElement() .withParent(CrStringLiteral::class.java) .withSuperParent(2, CrMethodRef::class.java)
对应每个 Pattern,我们需要实现一个 CompletionProvider 给出补全信息,比如一个固定返回关键字补全的 Provider:
val keywords = setOf("package", "class", "lang")class KeywordCompletionProvider : CompletionProvider<CompletionParameters>() { override fun addCompletions( parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet ) { keywords.forEach { keyword -> if (result.prefixMatcher.prefixMatches(keyword)) { // 添加一个 LookupElementBuilder,可以指定简单的样式 result.addElement(LookupElementBuilder.create(keyword).bold()) } } }}
掌握上述技能,诸如 class、package、method 等关键字,乃至方法名和字段名的补全就都很容易实现了。
比较 trick 的是包名和带有包名的类名的补全,它们形如 a.b.c.DEF。不同的是,每次输入 '.' 都会触发一次补全,而且要求在字符串开头直接输入“DE”也能正确联想并补全。限于篇幅不展开介绍了,详见 com.intellij.codeInsight.completion.JavaClassNameCompletionContributor 的实现。
格式化
格式化这件事上,IDEA 并没有直接使用 PSI 或者 ASTNode,而是基于二者建立了一套“Block”体系。所有缩进、间距的调整都是以 Block 为最小粒度进行的(一些复杂语言拆的太细,这样设计可以很好地降低实现复杂度,妙啊)。
这里的概念也不多,列举如下:
- ASTBlock:我们用现有的 ASTNode 树构建 Block,因此继承此基类;
- Indent:控制每行的缩进;
- Spacing:控制每个 Block 之间的间距策略(最小、最大空格,是否强制换行 / 不换行等);
- Wrap:单行长度过长时的折行策略;
- Alignment:自己在 Parent Block 中的对齐方向;
实际敲代码时,大部分时间花在 getSpacing 方法上,写出来效果类似这样:
override fun getSpacing(child1: Block?, child2: Block): Spacing{ /*...*/ return when { // between ',' and ref node1?.elementType == CodeRefElementTypes.COMMA && psi2 is CrRef -> Spacing.createSpacing(/*minSpaces*/0, /*maxSpaces*/0, /*minLineFeeds*/1, /*keepLineBreaks*/true, /*keepBlankLines*/1) // between '[', literal, ']' node1?.elementType == CodeRefElementTypes.L_BRACKET && psi2 is CrStringLiteral || psi1 is CrStringLiteral && node2?.elementType == CodeRefElementTypes.R_BRACKET -> Spacing.createSpacing(/*minSpaces*/0, /*maxSpaces*/0, /*minLineFeeds*/0, /*keepLineBreaks*/false, /*keepBlankLines*/0) }}
格式化属于说起来很简单,实现起来很头痛的东西。实操过程中,被迫把前面写好的 BNF 做了一波不小的调整,才达到理想效果。好在我们的语言比较简陋简洁,没踩到什么大坑,如果面向更复杂的语言,工作量将是指数级提升(参考 com.intellij.psi.formatter.java 包下的代码量)。
MarkdownX
上面罗列这么多内容,说白了只是对 Markdown 中代码块的增强方案,接下来 CodeRef 和 Markdown 终于要合体了。
实际上官方一直有对 Markdown 的支持(IDEA 内置,AS 可选安装),包含一整套语言实现和编辑器、预览器。这里重点说说其预览的生成流程,如图:
分为以下几步(逻辑在 org.jetbrains:markdown 依赖中,未开源):
- 利用 MarkdownParser 将文本解析成若干 ASTNode;
- 利用 HtmlGenerator 内置的 visitor 访问每个 ASTNode 生成 HTML 文本;
- 将生成的 HTML Document 设置给内置浏览器(如果有),最终呈现在屏幕上;
交代个背景:在本项目启动之初,IDEA 正处于 JavaFX-WebView 到 JCEF 的过渡期(直接导致了 AndroidStudio 4.0 左右的版本没有可用的内置 WebView 实现)。
上述方案总结有以下问题:
- 兼容性较差,部分 IDE 版本无法看到预览;
- 每次 MD 的变更都会触发全量 generateHtml,如果文档内容复杂度较高,将有性能瓶颈;
- 将 HTML 文本 set 给浏览器时没有 diff 逻辑,会触发页面 reload,同样可能导致性能问题(后来针对带有 JCEF 的 IDE 增加了 diff 能力,但并不是所有 IDE 都内置 JCEF);
综合考虑下,我们决定不直接使用原生插件,而是基于其创建新的语言“MarkdownX”,最大程度复用原本的能力,追加对 CodeRef 的支持,同时基于 Swing 自制一套类似 RecyclerView 的机制改善预览性能。
优化后的方案流程类似这样:
自制的方案有很多优势:
- 内存占用更低(浏览器 vs. JComponent)
- 性能更佳(局部刷新、控件复用等)
- 体验更佳(浏览器内置对<code>标签的支持过于基础,无法实现代码高亮、引用跳转等功能,原生控件不存在这些限制)
- 兼容性更佳(不解释)
CodeRef 支持
MarkdownX 只是表现为“新语言”,实现上依然复用 MarkdownParser 和 HtmlGenerator,主要区别只有文件扩展名和对 code-fence 的处理。
所谓 code-fence,即 Markdown 中使用 「```」 符号包裹的代码块。不同于原生实现,我们需要在生成预览时替换代码块的内容,并使内容随代码变化而变化。
实操上,我们需要实现一个 org.intellij.markdown.html.GeneratingProvider,简写如下:
class MarkDownXCodeFenceGeneratingProvider : GeneratingProvider { override fun processNode(visitor: HtmlGenerator.HtmlGeneratingVisitor, text: String, node: ASTNode) { visitor.consumeHtml("<pre>") var state = 0 // 用于后面遍历 children 的时候暂存状态 /* ...一些变量定义 */ for(child in childrenToConsider) { if (state == 1 && child.type in listOf(MarkdownTokenTypes.CODE_FENCE_CONTENT, MarkdownTokenTypes.EOL)) { /* ...拼接每行内容 */ } if (state == 0 && child.type == MarkdownTokenTypes.FENCE_LANG) { /* ...记录当前 code-fence 的语言 */ applicablePlugin = firstApplicablePlugin(language) // 找到可以处理当前语言的“插件” } if (state == 0 && child.type == MarkdownTokenTypes.EOL) { /* ...进入代码段,设置状态 */ state = 1 } } if (state == 1) { visitor.consumeTagOpen(node, "code", *attributes.toTypedArray()) if (language != null && applicablePlugin != null) { /* ...命中自定义处理逻辑(即 CodeRef)*/ visitor.consumeHtml(content) // 即由自定义逻辑生成的 Html } else { visitor.consumeHtml(codeFenceContent) // 默认内容 } } /* ...一些收尾 */ }}
可以看到,在遍历 node 的 children 后,就可以确定当前代码段的语言。如果语言为 CodeRef,就会走到前文提到的“预览文本生成”逻辑中,最后通过 visitor(相当于一个 HTML Builder)将自定义的内容拼接到 Html 中。
预览性能优化
考虑到 JList 并没有“item 回收”能力,在 List 实现上我们选择直接使用 Box。处理流程如下图:
机制分为两大步:
- Data 层将 HTML 的 body 拆分成若干部分,diff 后将变更通知给 View 层;
- View 层将变更的数据设置到 List 对应位置上,并尽可能复用已有的 ViewHolder。过程可能涉及 ViewHolder 的创建和删除;
目前我们针对文本、图片和代码创建了三种 ViewHolder:
- 文本:使用 JTextPane 配合 HTML + CSS 完成文字样式的还原;
- 图片:自定义 JComponent 进行缩放、绘制,保证图片居中且完整展示;
- 代码:以 IDE 提供的 Editor 作为基础,进行必要的设置与逻辑精简;
这里对 Editor 的处理花费了大量精力:
- 使用原代码文件作为 context 创建 PsiCodeFragment 作为内容填充 Editor,以保证代码中对原文件 import 过的类、方法、字段可被正常 resolve(这点很重要,如果用 Mock 的 Document 作为内容,绝大部分代码高亮和跳转都是不生效的);
- 设置合适的 HighlightingFilter,确保“没有报红”(将原文件作为 context 的代价是,当前代码片段的类极有可能被认为是类重复,并且代码结构也不一定合法,因此需要禁用“报红”级别的代码分析);
- 禁用 Intention,设置只读(提升性能,降低干扰);
- 禁用 Inspection 和 ExternalAnnotator;(两者是性能消耗的大户,后者包括 Android Lint 相关逻辑)
经过上述优化,实测大部分情况下预览都可以流畅展示 & 刷新了。但如果同时打开多个文档,或者“操作速度惊人”,还是会时不时出现长时间卡顿。分析一波发现,性能消耗主要出在 HTML 生成上。
由于 Markdown 语法限制(节点深度低),常规的 MD 转 HTML 性能开销有限。但回顾上文,我们对 codeRef 的处理会伴随大量 PSI resolve,复杂度暴涨,频繁的全量 generate 就不那么合适了。一个很自然的想法是为每段 codeRef 添加缓存,内容不变时直接使用缓存的内容。这样在修改文字段落时可以完全避开其他文件的语法解析,修改 codeRef 段落时也仅会刷新当前代码块的内容。
那么问题来了:若用户修改的不是文档文件,而是被引用的代码,则在缓存的作用下,预览并不会立刻改变。那么更进一步,如果向所引用的所有文件注册监听,在变更时刷新缓存,问题可否得解呢?事实上,这样做问题确实解决了,但引入了新的问题:如何释放文件监听?
此处插入背景:对 code-fence 内容的干预是基于 Visitor 模式回调完成的,因此作为 generator 本身是不知道本次处理的代码块与前一次、后一次回调是否由同一个变更引起。举个例子:一个文档中有 A、B、C 三个 codeRef 块,则在一次 HTML 生成过程中,generator 会收到三次回调,且没有任何手段可以得知这三次回调的关联性。
目前,我们只能在一次 HTML 生成前后通知 generator,在 generator 内部维护一个队列 + 计数器,不那么优雅地解决泄漏问题。
至此,插件的整体性能表现终于落到可接受范围内。
Gradle / Dokka Plugin
为了让受众更广、内容随时可读,把文档做到可导出、可自动化部署是非常必要的。方案上,我们选用同为 IntelliJ 出品的 Dokka 作为基础框架,利用其完善的数据流变换能力,高效地适配多输出格式的场景。
Dokka 流程扩展
Dokka 作为同时兼容 Kotlin 和 Java 的文档框架,“数据流水线”的思想和极强的可扩展性是其特点。代码转换到文档页面的流程如下:
每个节点都有至少一个 Extension Point,扩展起来非常灵活。
图中几个主要角色列举如下:
- Env:包含基于 Kotlin Compiler 和 IntelliJ-Core 扩展的代码分析器(用于输出 Document Models)、开发者自定义的插件等组件;
- Document Models:对 module、package、class、function、fields 等元素的抽象,呈树形组织,本质是一些 data class;
- Page Models:由 PageCreator 以 Document Models 为输入,创建的一系列对象,是对“页面”的封装,描述“页面”的结构;
- Renderer:用于将 Page Models 渲染成某种格式的产物(Dokka 内置的有 HTML、Markdown 等);
从上述内容可以看出,Dokka 原本的作用只是将代码转换为文档页面,并不原生支持转换文档文件(也确实没必要)。但在我们的场景下,MarkdownX 的渲染是依赖源码信息的,也就正好能用到 Dokka 的这部分能力。
通过重写 PageCreator,我们将含有 MarkdownX 文档的工程变成类似这样的节点树:
MdxPageNode 对应 MarkdownX 文档内容,包含若干类型的 children 分别代表不同类型的内容片段;
在创建 MdxPageNode 时,我们用类似前文 IDEA-Plugin 的做法,重写一个 org.jetbrains.dokka.base.parsers.Parser 并修改对 code-fence 的处理,改为调用到「基建」部分中生成 CodeRef 预览文本的代码,最终得到所需的文档文本。
飞书适配
得到页面内容后,结合 Dokka 自带的 HtmlRenderer,输出一份可用于部署的 HTML 产物就轻而易举了。但现状是,我们更希望能把文档收敛在飞书上,这就需要再编写一份针对飞书的自定义 Renderer。
考虑到自己处理页面的树形结构过于复杂,实际上我们基于内置的 DefaultRenderer 基类进行扩展:
abstract class DefaultRenderer<T>( protected val context: DokkaContext) : Renderer { abstract fun T.buildHeader(level: Int, node: ContentHeader, content: T.() -> Unit) abstract fun T.buildLink(address: String, content: T.() -> Unit) abstract fun T.buildList( node: ContentList, pageContext: ContentPage, sourceSetRestriction: Set<DisplaySourceSet>= null ) abstract fun T.buildNewLine() abstract fun T.buildResource(node: ContentEmbeddedResource, pageContext: ContentPage) abstract fun T.buildTable( node: ContentTable, pageContext: ContentPage, sourceSetRestriction: Set<DisplaySourceSet>= null ) abstract fun T.buildText(textNode: ContentText) abstract fun T.buildNavigation(page: PageNode) abstract fun buildPage(page: ContentPage, content: (T, ContentPage) -> Unit): String abstract fun buildError(node: ContentNode)}
上面只列出一部分了回调方法。
可以看到,该类的接口方式比较新颖:用 Visitor 的方式遍历页面节点树,再提供一系列 Builder/DSL 风格的待实现方法给开发者。对于这些 abstract function,内置的 HtmlRenderer 采用 kotlinx.html(一个 DSL 风格的 HTML 构建器)实现,这意味着我们也要实现一套 DSL 风格的飞书文档构建器。
飞书开放平台文档查看链接:https://open.feishu.cn/document/home/index。
DSL 的部分就不详述了,这里主要说说飞书的文档结构。众所周知,Markdown 在设计之初就是面向 Web 的,因此与 HTML 天生具有互转的能力。然而飞书文档的数据结构相对更像 Pdf、Docx 这类文件,拥有有限层级,相对扁平。举个例子,同样的文档内容,MdxPageNode 中结构长这样:
而飞书的结构长这样:
可见差异是巨大的。这部分差异的抹平全靠自定义的 FeishuRenderer,具体做法只能 case by case 介绍,限于篇幅就不展开了,大体思路就是对不兼容的节点进行展开或合并,穿插必要的子树遍历。
下面提两个特殊点的处理:图片和链接。
文档链接
写 Markdown 文档时,往往需要插入链接,指向其他的 Markdown 文档(一般使用相对路径)。这时,我们需要想办法把相对路径映射成飞书链接,而且需要在 Render 步骤之后进行,因为映射的时候需要知道对应文档的飞书链接是什么。
第一反应肯定就是对文档做拓扑排序了,按照依赖关系一个个上传文档。但这样需要文档间没有循环依赖,显然这是不能保证的(两篇文档相互引用还蛮常见的)。幸好,飞书文档提供了修改文档的接口,因此我们可以提前创建一批空文档,获取到空文档的链接后,再做相对路径的替换。换句话说,处理文档上传流程为:创建空文档-> 替换相对路径为对应文档链接 -> 修改文档内容。
图片
图片在 Markdown 中可以和文本并列,属于 Paragraph 的一种。而飞书文档结构中,图片属于 Gallery,只能独占一行,无法和文字同行。两种格式从实现上无法完全兼容。当前初步实现方案是在 Paragraph 的 Group 入口向下 DFS,找到所有图片,单提出来放在文本前面。效果嘛,只能忍忍了。
顺便一提,图片也需要上传并替换的逻辑,这部分与文档链接相似,不赘述了。
结语
以上就是文档套件的全部内容:我们基于 IntelliJ 技术栈,通过设计新语言、编写 IDE 插件、Gradle / Dokka 插件,形成一套完整的文档辅助解决方案,有效建立了文档与代码的关联性,大幅提升编写、阅读体验。
未来,我们会为框架引入更多实用性改进,包括:
- 添加图形化的代码元素选择器,降低语言学习、使用成本;
- 优化预览渲染效果,对齐 WebView;
- 探索针对部分框架(Dagger、Retrofit 等)的文档自动生成能力;
目前框架尚处内测阶段,正逐步扩大范围推广。待方案成熟、功能稳定后,我们会将方案整体开源,以服务更多用户,同时吸取来自社区的 Idea,敬请期待!
加入我们
我们是字节跳动直播营收客户端团队,专注礼物、PK、直播权益等业务,并深入探索渲染、架构、跨端、效率等技术方向。目前北京、深圳都有大量人才需要,欢迎投递简历至 zhangtianye.bugfree@bytedance.com 加入我们!
以上内容是关于感叹自己傻的句子和形容往后余生相守的句子的内容,小编幸苦为你编辑整理,喜欢的请点赞收藏把。